online multiplayer chess game (note server currently down)
premove support :sparkles:
closes #14
bendn 2022-08-10
parent 41383a1 · commit de183bf
-rw-r--r--Globals.gd7
-rw-r--r--Square.gd57
-rw-r--r--Square.tscn40
-rw-r--r--Utils.gd40
-rw-r--r--assets/silhouette/B.pngbin655 -> 561 bytes
-rw-r--r--assets/silhouette/K.pngbin651 -> 566 bytes
-rw-r--r--assets/silhouette/N.pngbin764 -> 628 bytes
-rw-r--r--assets/silhouette/P.pngbin556 -> 497 bytes
-rw-r--r--assets/silhouette/Q.pngbin1188 -> 802 bytes
-rw-r--r--assets/silhouette/R.pngbin564 -> 395 bytes
-rw-r--r--board/chess.gd151
-rw-r--r--networking/PacketHandler.gd3
-rw-r--r--piece/Piece.gd63
-rw-r--r--piece/check-circle.tres2
-rw-r--r--piece/move-circle.tres13
-rw-r--r--piece/takeable-circle.tres4
-rw-r--r--project.godot10
-rw-r--r--ui/board/Board.gd143
-rw-r--r--ui/board/Board.tscn8
-rw-r--r--ui/menus/tests/engine_test.gd21
-rw-r--r--ui/menus/tests/tests.gd7
-rw-r--r--ui/theme/main.themebin1977 -> 1943 bytes
22 files changed, 356 insertions, 213 deletions
diff --git a/Globals.gd b/Globals.gd
index b60db79..51e0512 100644
--- a/Globals.gd
+++ b/Globals.gd
@@ -5,7 +5,7 @@ var piece_set := "california"
var board_color1: Color = Color(0.870588, 0.890196, 0.901961)
var board_color2: Color = Color(0.54902, 0.635294, 0.678431)
var spectating := false
-var playing := false
+var playing := false setget , get_playing
var chat: Chat = null
var grid: Grid = null
@@ -15,7 +15,10 @@ func reset_vars() -> void:
grid = null
chat = null
spectating = false
- playing = false
+
+
+func get_playing() -> bool:
+ return has_node("/root/Game")
func _ready() -> void:
diff --git a/Square.gd b/Square.gd
index d8e1124..f0edd80 100644
--- a/Square.gd
+++ b/Square.gd
@@ -8,27 +8,20 @@ signal right_clicked
var move_indicators := []
var square: String
-onready var circle: TextureRect = $CircleHolder/Circle
+onready var circle: TextureRect = $Circle
onready var move_indicator: ColorRect = $MoveIndicator
+onready var premove_indicator: ColorRect = $PremoveIndicator
func _ready() -> void:
- connect("clicked", self, "clicked")
move_indicator.color = Globals.grid.last_move_indicator_color
- circle.material.set_shader_param("color", Globals.grid.overlay_color)
- if Globals.spectating:
- mouse_default_cursor_shape = CURSOR_FORBIDDEN
- else:
- mouse_default_cursor_shape = CURSOR_POINTING_HAND
- size()
-
-
-func size():
- circle.rect_min_size = Globals.grid.piece_size / 4
+ premove_indicator.color = Globals.grid.premove_color
+ mouse_default_cursor_shape = CURSOR_FORBIDDEN if Globals.spectating else CURSOR_POINTING_HAND
+ Events.connect("turn_over", self, "clear_move_indicators")
func check_piece_above() -> bool:
- return is_instance_valid(Globals.grid.get_piece(square)) if Globals.playing else false
+ return is_instance_valid(Globals.grid.get_piece(square))
func _gui_input(event: InputEvent):
@@ -38,6 +31,10 @@ func _gui_input(event: InputEvent):
func _focus_exited():
+ clear_move_indicators()
+
+
+func clear_move_indicators():
if check_piece_above():
Globals.grid.get_piece(square).background.hide()
for m in move_indicators:
@@ -46,14 +43,28 @@ func _focus_exited():
move_indicators.clear()
-func clicked():
+func show_move_indicators():
+ clear_move_indicators()
+ var b = Globals.grid
+ var p = b.get_piece(square)
+ p.background.show()
+ var movs = b.chess.__generate_moves({"square": square})
+ for m in movs:
+ var i = b.board[m.to].frame if m.flags & Chess.BITS.CAPTURE else b.background_array[m.to].circle
+ move_indicators.append(i)
+ i.show()
+
+
+func show_premove_indicators():
+ clear_move_indicators()
var b = Globals.grid
- if check_piece_above() and b.chess.turn == Globals.team and not Globals.spectating:
- var p = b.get_piece(square)
- if p.color == Globals.team:
- p.background.show()
- var movs = b.chess.__generate_moves({"square": square, "verbose": true})
- for m in movs:
- var i = b.board[m.to].frame if m.flags & Chess.BITS.CAPTURE else b.background_array[m.to].circle
- move_indicators.append(i)
- i.show()
+ var p = b.get_piece(square)
+ p.background.show()
+ var movs = b.chess.piece_moves(square, p.type, Globals.team, false)
+ for m in movs:
+ var _p = b.board[m.to]
+ var i = b.background_array[m.to].circle
+ if is_instance_valid(_p):
+ i = _p.frame
+ move_indicators.append(i)
+ i.show()
diff --git a/Square.tscn b/Square.tscn
index e245893..f708170 100644
--- a/Square.tscn
+++ b/Square.tscn
@@ -1,30 +1,7 @@
-[gd_scene load_steps=5 format=2]
+[gd_scene load_steps=3 format=2]
[ext_resource path="res://Square.gd" type="Script" id=1]
-[ext_resource path="res://assets/blank.png" type="Texture" id=2]
-
-[sub_resource type="Shader" id=2]
-code = "shader_type canvas_item;
-
-uniform float amt : hint_range(0.0, 1.0);
-uniform vec4 color : hint_color;
-
-void fragment()
-{
- if (distance(UV, vec2(0.5,0.5)) > amt/2.0)
- {
- COLOR = vec4(0.0);
- }
- else
- {
- COLOR = vec4(color);
- }
-}"
-
-[sub_resource type="ShaderMaterial" id=3]
-shader = SubResource( 2 )
-shader_param/amt = 1.0
-shader_param/color = Color( 0.431373, 0.584314, 0.388235, 0.639216 )
+[ext_resource path="res://piece/move-circle.tres" type="Texture" id=2]
[node name="Square" type="ColorRect"]
anchor_right = 1.0
@@ -42,20 +19,19 @@ anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
-[node name="CircleHolder" type="CenterContainer" parent="."]
+[node name="PremoveIndicator" type="ColorRect" parent="."]
+visible = false
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
-[node name="Circle" type="TextureRect" parent="CircleHolder"]
+[node name="Circle" type="TextureRect" parent="."]
visible = false
-material = SubResource( 3 )
-margin_left = 25.0
-margin_top = 25.0
-margin_right = 25.0
-margin_bottom = 25.0
+anchor_right = 1.0
+anchor_bottom = 1.0
mouse_filter = 2
texture = ExtResource( 2 )
expand = true
+stretch_mode = 6
[connection signal="focus_exited" from="." to="." method="_focus_exited"]
diff --git a/Utils.gd b/Utils.gd
index 32fbf0f..a4930f4 100644
--- a/Utils.gd
+++ b/Utils.gd
@@ -9,8 +9,10 @@ static func compile(src: String) -> RegEx:
return regex
-static func str_bool(string: String) -> bool:
- return string.to_lower().strip_edges() in ["true", "1", "on", "yes", "y", ""]
+static func str_bool(string: String, extra_cases: PoolStringArray = []) -> bool:
+ var cases = ["true", "1", "on", "yes", "y"]
+ cases.append_array(extra_cases)
+ return string.to_lower().strip_edges() in cases
func expand_color(color: String) -> String:
@@ -88,12 +90,23 @@ func cli() -> void:
Arg.new(
{
triggers = ["--debug", "-D"],
+ default = "yes",
n_args = 1,
help = "toggle debug mode",
arg_names = "enabled",
}
)
)
+ parser.add_argument(
+ Arg.new(
+ {
+ triggers = ["--test", "-t", "-T"],
+ n_args = 0,
+ help = "run engine tests",
+ action = "store_true",
+ }
+ )
+ )
var args = parser.parse_arguments()
Debug.debug = str_bool(args["debug"]) if args.has("debug") else OS.is_debug_build()
if args.get("help", false):
@@ -102,24 +115,25 @@ func cli() -> void:
elif args.get("version", false):
print("chess %s" % get_version())
get_tree().quit() # dont wait
+ elif args.get("test", false):
+ print("Starting tests")
+ TestButton.TestChess.new()
+ print("Tests passed")
+ get_tree().quit() # dont wait
elif args.has("host") or args.has("join"):
if !internet:
printerr("No internet")
get_tree().quit()
yield(PacketHandler, "connection_established")
if args.has("host") and args.host:
- print("hosting game: %s" % args.host)
if PacketHandler.lobby.validate_text(args.host):
- var s = args.get("moves", PoolStringArray()).join(" ")
- var move_list = Pgn.parse(s, false).moves
- if move_list:
- print("with moves: %s" % move_list)
- var clr = (
- (true if args.color.to_lower() in ["w", "white"] or str_bool(args.color) else false)
- if args.has("color")
- else (true)
- ) # default white
- prints("as", "white" if clr else "black")
+ var pgn_input = args.get("moves", PoolStringArray()).join(" ")
+ var move_list = Pgn.parse(pgn_input, false).moves
+ var clr = str_bool(args.color, ["w", "white"]) if args.has("color") else true # default white
+ var string = "hosting game: %s" % args.host
+ string += ", with moves: %s" % move_list if move_list else ""
+ string += ", as " + ("white" if clr else "black")
+ print(string)
PacketHandler.host_game(args.host, clr, move_list)
return
elif args.has("join") and args.join:
diff --git a/assets/silhouette/B.png b/assets/silhouette/B.png
index 6f4826c..7de8773 100644
--- a/assets/silhouette/B.png
+++ b/assets/silhouette/B.png
Binary files differ
diff --git a/assets/silhouette/K.png b/assets/silhouette/K.png
index 2f0a7df..512d162 100644
--- a/assets/silhouette/K.png
+++ b/assets/silhouette/K.png
Binary files differ
diff --git a/assets/silhouette/N.png b/assets/silhouette/N.png
index 3a2a021..037f77c 100644
--- a/assets/silhouette/N.png
+++ b/assets/silhouette/N.png
Binary files differ
diff --git a/assets/silhouette/P.png b/assets/silhouette/P.png
index 0497bd4..e19f5ea 100644
--- a/assets/silhouette/P.png
+++ b/assets/silhouette/P.png
Binary files differ
diff --git a/assets/silhouette/Q.png b/assets/silhouette/Q.png
index 3ff64e1..e5e36c7 100644
--- a/assets/silhouette/Q.png
+++ b/assets/silhouette/Q.png
Binary files differ
diff --git a/assets/silhouette/R.png b/assets/silhouette/R.png
index 328525f..b950502 100644
--- a/assets/silhouette/R.png
+++ b/assets/silhouette/R.png
Binary files differ
diff --git a/board/chess.gd b/board/chess.gd
index 882c56a..fe60e25 100644
--- a/board/chess.gd
+++ b/board/chess.gd
@@ -119,7 +119,7 @@ static func stripped_san(move: String) -> String:
# this func is used to uniquely identify ambiguous moves
-func get_disambiguator(move: Dictionary, moves: Array) -> String:
+static func get_disambiguator(move: Dictionary, moves: Array) -> String:
var from: int = move.from
var to: int = move.to
var piece: String = move.piece
@@ -171,6 +171,67 @@ static func infer_piece_type(san: String) -> String:
return piece_type
+# WARNING: If `localized` is disabled, you must use localize_piece_move on the returned piece object.
+func piece_moves(_square_: String, piece: String, color := turn, localize := true) -> Array:
+ var moves := []
+ var second_rank := {b = RANK_7, w = RANK_2}
+ var sq: int = SQUARE_MAP[_square_]
+ if piece == PAWN:
+ # single square, non capturing
+ var square = sq + PAWN_OFFSETS[color][0]
+ if square <= SQUARE_MAP.h1:
+ __add_piece_move(moves, sq, square, piece)
+
+ # double square
+ var _square: int = sq + PAWN_OFFSETS[color][1]
+ if second_rank[color] == rank(sq):
+ __add_piece_move(moves, sq, _square, piece, BITS.BIG_PAWN)
+
+ # pawn captures
+ for j in range(2, 4):
+ var _square: int = sq + PAWN_OFFSETS[color][j]
+ if _square & 0x88: # off the board
+ continue
+ __add_piece_move(moves, sq, _square, piece, BITS.CAPTURE)
+ else:
+ for offset in PIECE_OFFSETS[piece]:
+ var square := sq
+ while true:
+ square += offset
+ if square & 0x88:
+ break
+
+ __add_piece_move(moves, sq, square, piece)
+ if piece in KNIGHT + KING:
+ break
+
+ if piece == KING and _square_ in ["e1", "e8"]:
+ __add_piece_move(moves, sq, sq + 2, piece, BITS.KSIDE_CASTLE)
+ __add_piece_move(moves, sq, sq - 2, piece, BITS.QSIDE_CASTLE)
+
+ if localize:
+ for i in range(len(moves)):
+ moves[i] = localize_piece_move(moves[i])
+ return moves
+
+
+static func __add_piece_move(moves: Array, from: int, to: int, piece: String, flags := BITS.NORMAL) -> void:
+ var mov = {from = from, to = to, flags = flags}
+ if piece == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1):
+ for p in [QUEEN, ROOK, BISHOP, KNIGHT]:
+ var m = mov.duplicate()
+ m["promotion"] = p
+ m.flags |= BITS.PROMOTION
+ moves.append(m)
+ else:
+ moves.append(mov)
+
+
+# Please only use the returned object if the team the move was created for is the current turn.
+func localize_piece_move(piece_move: Dictionary) -> Dictionary:
+ return __build_move(piece_move.from, piece_move.to, piece_move.flags, piece_move.get("promotion", ""))
+
+
###
### utility functions
###
@@ -182,6 +243,10 @@ static func file(i: int) -> int:
return i & 15
+static func vecfrom0x88(i: int) -> Vector2:
+ return Vector2(file(i), rank(i))
+
+
static func vec2algebraic(pos: Vector2) -> String:
var column := "abcdefgh"[pos.x]
var row := str(round(8 - pos.y))
@@ -206,7 +271,7 @@ static func offset(pos, offset: Vector2) -> String:
if typeof(pos) == TYPE_STRING: # algbraic
return vec2algebraic(algebraic2vec(pos) + offset)
elif typeof(pos) == TYPE_INT: # board pos
- return offset(algebraic(pos), offset) # supreme lazy
+ return vec2algebraic(vecfrom0x88(pos) + offset)
return ""
@@ -357,38 +422,35 @@ func remove(square) -> Dictionary:
return piece
-# warning-ignore:shadowed_variable
-func __build_move(board: Array, from: int, to: int, flags: int, promotion := ""):
+func __add_move(moves: Array, from: int, to: int, flags := BITS.NORMAL, b := board) -> void:
+ # if pawn promotion
+ if b[from].type == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1):
+ for p in [QUEEN, ROOK, BISHOP, KNIGHT]:
+ moves.append(__build_move(from, to, flags, p))
+ else:
+ moves.append(__build_move(from, to, flags))
+
+
+func __build_move(from: int, to: int, flags: int = BITS.NORMAL, promotion := "", _board: Array = board):
var move := {
color = turn,
from = from,
to = to,
flags = flags,
- piece = board[from].type,
+ piece = _board[from].type,
}
if promotion:
move.flags |= BITS.PROMOTION
move.promotion = promotion
- if board[to]:
- move.captured = board[to].type
+ if _board[to]:
+ move.captured = _board[to].type
elif flags & BITS.EP_CAPTURE:
move.captured = PAWN
return move
-# warning-ignore:shadowed_variable
-func __add_move(board: Array, moves: Array, from: int, to: int, flags: int) -> void:
- # if pawn promotion
- if board[from].type == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1):
- var pieces := [QUEEN, ROOK, BISHOP, KNIGHT]
- for p in pieces:
- moves.append(__build_move(board, from, to, flags, p))
- else:
- moves.append(__build_move(board, from, to, flags))
-
-
func __generate_moves(options := {}) -> Array:
var moves := []
var us := turn
@@ -430,12 +492,12 @@ func __generate_moves(options := {}) -> Array:
# single square, non capturing
var square = i + PAWN_OFFSETS[us][0]
if square <= SQUARE_MAP.h1 and board[square] == null:
- __add_move(board, moves, i, square, BITS.NORMAL)
+ __add_move(moves, i, square, BITS.NORMAL)
# double square
var _square: int = i + PAWN_OFFSETS[us][1]
if second_rank[us] == rank(i) && board[_square] == null:
- __add_move(board, moves, i, _square, BITS.BIG_PAWN)
+ __add_move(moves, i, _square, BITS.BIG_PAWN)
# pawn captures
for j in range(2, 4):
@@ -444,9 +506,9 @@ func __generate_moves(options := {}) -> Array:
continue
if board[_square] != null && board[_square].color == them:
- __add_move(board, moves, i, _square, BITS.CAPTURE)
+ __add_move(moves, i, _square, BITS.CAPTURE)
elif _square == ep_square:
- __add_move(board, moves, i, ep_square, BITS.EP_CAPTURE)
+ __add_move(moves, i, ep_square, BITS.EP_CAPTURE)
elif piece_type == "-1" || piece_type == piece.type:
for offset in PIECE_OFFSETS[piece.type]:
var square := i
@@ -457,11 +519,11 @@ func __generate_moves(options := {}) -> Array:
break
if board[square] == null:
- __add_move(board, moves, i, square, BITS.NORMAL)
+ __add_move(moves, i, square, BITS.NORMAL)
else:
if board[square].color == us:
break
- __add_move(board, moves, i, square, BITS.CAPTURE)
+ __add_move(moves, i, square, BITS.CAPTURE)
break
# break, if knight or king
@@ -484,7 +546,7 @@ func __generate_moves(options := {}) -> Array:
&& !__attacked(them, castling_from + 1)
&& !__attacked(them, castling_to)
):
- __add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE)
+ __add_move(moves, kings[us], castling_to, BITS.KSIDE_CASTLE)
# queen-side castling
if castling[us] & BITS.QSIDE_CASTLE:
@@ -499,7 +561,7 @@ func __generate_moves(options := {}) -> Array:
&& !__attacked(them, castling_from - 1)
&& !__attacked(them, castling_to)
):
- __add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE)
+ __add_move(moves, kings[us], castling_to, BITS.QSIDE_CASTLE)
# return all pseudo-legal moves (this includes moves that allow the king
# to be captured)
@@ -714,10 +776,7 @@ func __make_move(move: Dictionary):
# if ep capture, remove the captured pawn
if move.flags & BITS.EP_CAPTURE:
- if turn == BLACK:
- board[move.to - 16] = null
- else:
- board[move.to + 16] = null
+ board[move.to + (-16 if us == BLACK else 16)] = null
# if pawn promotion, replace with new piece
if move.flags & BITS.PROMOTION:
@@ -756,24 +815,12 @@ func __make_move(move: Dictionary):
break
# if big pawn move, update the en passant square
- if move.flags & BITS.BIG_PAWN:
- if turn == "b":
- ep_square = move.to - 16
- else:
- ep_square = move.to + 16
- else:
- ep_square = EMPTY
+ ep_square = (move.to + (-16 if turn == BLACK else 16)) if move.flags & BITS.BIG_PAWN else EMPTY
# reset the 50 move counter if a pawn is moved or a piece is captured
- if move.piece == PAWN:
- half_moves = 0
- elif move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE):
- half_moves = 0
- else:
- half_moves += 1
+ half_moves = 0 if move.piece == PAWN or move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE) else (half_moves + 1)
- if turn == BLACK:
- fullmoves += 1
+ fullmoves += 1 if turn == BLACK else 0
turn = __swap_color(turn)
@@ -785,8 +832,6 @@ func __undo_move() -> Dictionary:
var move: Dictionary = old.move
kings = old.kings
turn = old.turn
- if typeof(old.castling.w) != TYPE_INT || typeof(old.castling.b) != TYPE_INT:
- breakpoint
castling = old.castling
ep_square = old.ep_square
half_moves = old.half_moves
@@ -798,16 +843,10 @@ func __undo_move() -> Dictionary:
board[move.from] = board[move.to]
board[move.from].type = move.piece # to undo any promotions
board[move.to] = null
-
if move.flags & BITS.CAPTURE:
board[move.to] = {type = move.captured, color = them}
elif move.flags & BITS.EP_CAPTURE:
- var index
- if us == BLACK:
- index = move.to - 16
- else:
- index = move.to + 16
- board[index] = {type = PAWN, color = them}
+ board[move.to + (-16 if us == BLACK else 16)] = {type = PAWN, color = them}
if move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE):
var castling_to
@@ -888,7 +927,7 @@ func __move_from_san(move, sloppy := false) -> Dictionary:
to = matches[3]
promotion = matches[4]
- if from.length() == 1:
+ if from and from.length() == 1:
overly_disambiguated = true
var piece_type := infer_piece_type(clean_move)
@@ -1056,7 +1095,7 @@ func pgn() -> String:
move_string += " %s" % __move_to_san(move, __generate_moves({legal = true}))
__make_move(move)
-
+
if move_string.length():
moves.append(move_string)
diff --git a/networking/PacketHandler.gd b/networking/PacketHandler.gd
index 5734e28..47a122e 100644
--- a/networking/PacketHandler.gd
+++ b/networking/PacketHandler.gd
@@ -161,7 +161,7 @@ func handle_result(accepted, resultstring: String) -> bool:
func go_back(error: String, isok: bool) -> void:
Globals.reset_vars()
- if has_node("/root/Game"):
+ if Globals.playing:
$"/root/Game".queue_free()
set_lobby_status(error, isok)
lobby.toggle(true)
@@ -170,7 +170,6 @@ func go_back(error: String, isok: bool) -> void:
func _start_game() -> void:
- Globals.playing = true
set_hosting(false)
var board: Control = load("res://ui/board/Game.tscn").instance()
get_tree().get_root().add_child(board)
diff --git a/piece/Piece.gd b/piece/Piece.gd
index 081b269..3196b7c 100644
--- a/piece/Piece.gd
+++ b/piece/Piece.gd
@@ -13,8 +13,7 @@ onready var anim = $AnimationPlayer
onready var rotate = $RotatePlayer
# for pawn promotion
-signal promotion_decided
-var promote_to := ""
+signal promotion_decided(promote_to)
func size() -> void: # size the control
@@ -27,14 +26,20 @@ func size() -> void: # size the control
func _ready():
load_texture()
-
- frame.modulate = Globals.grid.overlay_color
background.color = Globals.grid.overlay_color
if type == Chess.KING:
Events.connect("turn_over", self, "check_in_check")
size()
+ Events.connect("turn_over", self, "turn_over")
+
+
+func turn_over():
+ if Globals.grid.chess.turn == Globals.team:
+ background.color = Globals.grid.overlay_color
+ else:
+ background.color = Globals.grid.premove_color
func check_in_check():
@@ -42,32 +47,36 @@ func check_in_check():
func _pressed(p: String) -> void:
- promote_to = p
- emit_signal("promotion_decided")
- queue_free()
-
-
-func open_promotion_previews():
- var popup := PopupPanel.new()
- popup.name = "previews"
- popup.popup_exclusive = true
- popup.add_stylebox_override("panel", StyleBoxEmpty.new())
- var previews := VBoxContainer.new()
- previews.name = "previews"
- previews.add_constant_override("separation", 0)
- popup.add_child(previews)
- add_child(popup)
- for p in "QNRB":
- var newsprite := PromotionPreview.new()
- newsprite.hint_tooltip = p
- var img_path = "res://assets/pieces/%s/%s%s.png" % [Globals.piece_set, color, p]
- newsprite.texture_normal = load(img_path)
- newsprite.name = p
- newsprite.connect("pressed", self, "_pressed", [p])
- previews.add_child(newsprite)
+ emit_signal("promotion_decided", p.to_lower())
+
+
+func open_promotion_previews(darken: ColorRect):
+ darken.show()
+ var popup = get_node_or_null("previews")
+ if not popup:
+ popup = PopupPanel.new()
+ popup.name = "previews"
+ popup.popup_exclusive = true
+ popup.add_stylebox_override("panel", StyleBoxEmpty.new())
+ var previews := VBoxContainer.new()
+ previews.name = "previews"
+ previews.add_constant_override("separation", 0)
+ popup.add_child(previews)
+ add_child(popup)
+ for p in "QNRB":
+ var newsprite := PromotionPreview.new()
+ newsprite.hint_tooltip = p
+ var img_path = "res://assets/pieces/%s/%s%s.png" % [Globals.piece_set, color, p]
+ newsprite.texture_normal = load(img_path)
+ newsprite.name = p
+ newsprite.connect("pressed", self, "_pressed", [p])
+ previews.add_child(newsprite)
var rect = Rect2(rect_global_position, Vector2(Globals.grid.piece_size.x, Globals.grid.piece_size.y * 4))
popup.popup(rect)
+ yield(self, "promotion_decided")
+ darken.hide()
+ popup.hide()
func load_texture(path := "res://assets/pieces/%s/%s%s.png" % [Globals.piece_set, color, type.to_upper()]) -> void:
diff --git a/piece/check-circle.tres b/piece/check-circle.tres
index f1ec6d7..b02a3c6 100644
--- a/piece/check-circle.tres
+++ b/piece/check-circle.tres
@@ -5,7 +5,7 @@ offsets = PoolRealArray( 0, 0.3, 0.9, 1 )
colors = PoolColorArray( 0.941176, 0, 0, 1, 0.905882, 0, 0, 1, 0.662745, 0, 0, 0, 0.619608, 0, 0, 0 )
[resource]
-flags = 28
+flags = 12
gradient = SubResource( 1 )
width = 80
height = 80
diff --git a/piece/move-circle.tres b/piece/move-circle.tres
new file mode 100644
index 0000000..4691152
--- /dev/null
+++ b/piece/move-circle.tres
@@ -0,0 +1,13 @@
+[gd_resource type="GradientTexture2D" load_steps=2 format=2]
+
+[sub_resource type="Gradient" id=1]
+offsets = PoolRealArray( 0.163814, 0.210269 )
+colors = PoolColorArray( 0.0784314, 0.333333, 0.117647, 0.498039, 0.00425922, 0.0181017, 0.00638883, 0.0564515 )
+
+[resource]
+flags = 12
+gradient = SubResource( 1 )
+width = 80
+height = 80
+fill = 1
+fill_from = Vector2( 0.5, 0.5 )
diff --git a/piece/takeable-circle.tres b/piece/takeable-circle.tres
index fcc0ea6..aff70a7 100644
--- a/piece/takeable-circle.tres
+++ b/piece/takeable-circle.tres
@@ -1,8 +1,8 @@
[gd_resource type="GradientTexture2D" load_steps=2 format=2]
[sub_resource type="Gradient" id=1]
-offsets = PoolRealArray( 0.7, 0.751515 )
-colors = PoolColorArray( 0, 0, 0, 0, 0.0784314, 0.333333, 0.117647, 1 )
+offsets = PoolRealArray( 0.7, 0.716381 )
+colors = PoolColorArray( 0, 0, 0, 0, 0.0784314, 0.333333, 0.117647, 0.498039 )
[resource]
flags = 12
diff --git a/project.godot b/project.godot
index 8d19068..26ac583 100644
--- a/project.godot
+++ b/project.godot
@@ -204,6 +204,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://ui/Status.gd"
}, {
+"base": "Button",
+"class": "TestButton",
+"language": "GDScript",
+"path": "res://ui/menus/tests/engine_test.gd"
+}, {
"base": "Container",
"class": "TextEditor",
"language": "GDScript",
@@ -269,6 +274,7 @@ _global_script_class_icons={
"SaveLoader": "",
"SliderButton": "",
"StatusLabel": "",
+"TestButton": "",
"TextEditor": "",
"UndoButton": "",
"UserPanel": "",
@@ -316,6 +322,10 @@ window/size/height=800
window/stretch/mode="2d"
window/stretch/aspect="keep"
+[editor]
+
+main_run_args="--join __tests__"
+
[editor_plugins]
enabled=PoolStringArray( )
diff --git a/ui/board/Board.gd b/ui/board/Board.gd
index a060366..19a9047 100644
--- a/ui/board/Board.gd
+++ b/ui/board/Board.gd
@@ -13,15 +13,15 @@ signal remove_last
var move_indicators: PoolIntArray = []
var rot: float = 0
-
var piece_size: Vector2
-export(Color) var overlay_color := Color(0.078431, 0.333333, 0.117647, 0.498039)
-export(Color) var last_move_indicator_color := Color(0.74902, 0.662745, 0.223529, 0.498039)
-export(Color) var last_move_take_indicator := Color(0.74902, 0.407843, 0.223529, 0.498039)
-export(Color) var clockrunning_color := Color(0.219608, 0.278431, 0.133333)
-export(Color) var clockrunninglow := Color(0.47451, 0.172549, 0.164706)
-export(Color) var clocklow := Color(0.313726, 0.156863, 0.14902)
+export(Color) var overlay_color: Color
+export(Color) var premove_color: Color
+export(Color) var last_move_indicator_color: Color
+export(Color) var last_move_take_indicator: Color
+export(Color) var clockrunning_color: Color
+export(Color) var clockrunninglow: Color
+export(Color) var clocklow: Color
var board = [] # has `get_piece(algebraic position)` and `set_piece(algebraic position)` for ease of use
@@ -34,19 +34,22 @@ func set_piece(alg: String, p: Piece) -> void:
board[Chess.SQUARE_MAP[alg]] = p
-var flipped = false
-var labels = {numbers = [], letters = []}
-var background_array = []
-var last_clicked
+var flipped := false
+var labels := {numbers = [], letters = []}
+var background_array := []
+var last_clicked: Piece
+var premove: Dictionary = {}
var check_circle: GradientTexture2D = load("res://piece/check-circle.tres")
var take_circle: GradientTexture2D = load("res://piece/takeable-circle.tres")
+var move_circle: GradientTexture2D = load("res://piece/move-circle.tres")
-onready var sidebar := get_node_or_null("%Sidebar")
onready var game: GameUI = owner if owner is GameUI else null
+onready var sidebar := game.get_node_or_null("%Sidebar") if game else null
onready var darken = $Darken
onready var foreground := $Foreground
onready var background := $Background
onready var pieces := $Pieces
+onready var arrows := $"%Arrows"
var chess := Chess.new()
@@ -62,6 +65,8 @@ func _exit_tree():
func _process(_delta):
rect_rotation = rot
foreground.rect_rotation = rot
+ if Input.is_action_just_pressed("debug"):
+ print(chess.ascii())
func _resized():
@@ -73,6 +78,8 @@ func _resized():
check_circle.height = piece_size.y
take_circle.width = piece_size.x
take_circle.height = piece_size.y
+ move_circle.width = piece_size.x
+ move_circle.height = piece_size.y
if foreground:
rect_pivot_offset = (piece_size * 8) / 2
foreground.rect_pivot_offset = rect_pivot_offset
@@ -81,27 +88,27 @@ func _resized():
Log.debug("Resizing board")
+func set_take_move_circle_color(
+ color: Color = Color(overlay_color.r, overlay_color.g, overlay_color.b, .65)
+) -> void:
+ take_circle.gradient.colors[1] = color
+ move_circle.gradient.colors[0] = color
+
+
func _ready():
- take_circle.gradient.colors[1] = Color(overlay_color.r, overlay_color.g, overlay_color.b, 1)
+ set_take_move_circle_color()
_resized()
Events.connect("turn_over", self, "_on_turn_over")
- PacketHandler.connect("move_data", self, "move")
+ PacketHandler.connect("move_data", self, "move", [false, false])
create_pieces()
create_squares()
create_labels()
func resize_board():
- resize_squares()
resize_pieces()
-func resize_squares() -> void:
- for i in Chess.SQUARE_MAP.values():
- var square: BackgroundSquare = background_array[i]
- square.size()
-
-
func create_squares() -> void: # create the board
background_array.resize(128)
for i in Chess.SQUARE_MAP.values():
@@ -112,9 +119,9 @@ func create_squares() -> void: # create the board
square.hint_tooltip = alg
square.color = (Globals.board_color1 if Chess.square_color(alg) == "light" else Globals.board_color2) # set the color
background.add_child(square) # add the square to the background
- square.connect("clicked", self, "square_clicked", [alg]) # connect the clicked event
+ square.connect("clicked", self, "square_clicked", [square]) # connect the clicked event
background_array[i] = square # add the square to the background array
- find_node("Arrows")._setup(self) # initialize the arrows
+ arrows._setup(self) # initialize the arrows
func create_labels() -> void:
@@ -237,24 +244,50 @@ func flip_board() -> void:
flip_labels()
-func square_clicked(clicked_square: String) -> void:
- if chess.turn != Globals.team or Globals.spectating:
+func square_clicked(clicked_square: BackgroundSquare) -> void:
+ if Globals.spectating:
return
- var p := get_piece(clicked_square)
- if !p or p.color != Globals.team:
- if !is_instance_valid(last_clicked):
- return
+
+ var p := get_piece(clicked_square.square)
+
+ if chess.turn != Globals.team and is_instance_valid(last_clicked):
+ # PREMOVE AREA
+ var p_sq: int = Chess.SQUARE_MAP[clicked_square.square]
+ for m in chess.piece_moves(last_clicked.position, last_clicked.type, Globals.team):
+ if m.to == p_sq && m.from == Chess.SQUARE_MAP[last_clicked.position]:
+ if "from" in premove and "to" in premove:
+ background_array[premove.from].premove_indicator.hide() # hide premove indicators
+ background_array[premove.to].premove_indicator.hide()
+ if premove && premove.from == m.from && premove.to == m.to:
+ premove = {}
+ Log.debug("De-selected premove")
+ else:
+ premove = m
+ background_array[premove.from].premove_indicator.show()
+ background_array[premove.to].premove_indicator.show()
+ if premove.flags & Chess.BITS.PROMOTION:
+ p.open_promotion_previews(darken)
+ premove.promotion = yield(p, "promotion_decided")
+ Log.debug("Selected premove: %s" % premove)
+ clear_last_clicked()
+ return
+ elif (!p or p.color != Globals.team) and is_instance_valid(last_clicked):
+ # Attempt to make the move (NORMAL MOVE AREA)
for m in chess.moves({square = last_clicked.position, verbose = true}):
- if m.to == clicked_square && m.from == last_clicked.position:
- move(m.san, false)
- break
- clear_last_clicked()
- return
- last_clicked = p
+ if m.to == clicked_square.square && m.from == last_clicked.position:
+ move(m.san)
+ clear_last_clicked()
+ return
+
+ if p and p.color == Globals.team:
+ if chess.turn != Globals.team:
+ clicked_square.show_premove_indicators()
+ else:
+ clicked_square.show_move_indicators()
+ last_clicked = p
-func move(san: String, is_recieved_move := true) -> void:
- resize_board()
+func move(san: String, send := true, create_promotion_input := true) -> void:
var sound_handled = false
var move_0x88 = chess.__move_from_san(san, true)
var valid_moves = chess.moves({square = chess.algebraic(move_0x88.from), stripped = true})
@@ -279,23 +312,19 @@ func move(san: String, is_recieved_move := true) -> void:
get_piece(rook_pos).move(Chess.offset(move_0x88.to, Vector2(1, 0)))
if move_0x88.flags & Chess.BITS.PROMOTION: #promotion wow
var p: Piece = yield(board[move_0x88.from].move(Chess.algebraic(move_0x88.to), true), "completed")
- if !is_recieved_move: # was my turn, this is my move
- darken.show()
- p.open_promotion_previews()
- yield(p, "promotion_decided") # piece kills itself now
- move_0x88["promotion"] = p.promote_to
+ if create_promotion_input:
+ p.open_promotion_previews(darken)
+ move_0x88.promotion = yield(p, "promotion_decided")
san = chess.__move_to_san(move_0x88) # update the san with new promotion data
- darken.hide()
+ p.queue_free()
else: # was opponents turn, this is opponents move: promotion is already chosen
p.queue_free()
- # the move animation is useless if its not my turn
- # but it changes p.position, so its usefull.
make_piece(p.position, move_0x88.promotion, p.color)
SoundFx.play("Move" if move_0x88.flags & Chess.BITS.NORMAL else "Capture")
sound_handled = true
else: # not promotion: from **always** moves to `to`
var _p = board[move_0x88.from].move(Chess.algebraic(move_0x88.to))
- if !is_recieved_move:
+ if send:
PacketHandler.send_mov(san)
if !sound_handled:
SoundFx.play("Move")
@@ -345,6 +374,28 @@ func undo(two: bool = false) -> void:
func _on_turn_over():
+ if get_parent() == get_viewport(): # for testing
+ Globals.team = chess.turn
+
+ if Globals.grid.chess.turn == Globals.team:
+ set_take_move_circle_color()
+ # use the premove if possible
+ if premove:
+ background_array[premove.from].premove_indicator.hide()
+ background_array[premove.to].premove_indicator.hide()
+ if board[premove.from]: # see if its valid
+ if premove.flags & (Chess.BITS.CAPTURE | Chess.BITS.EP_CAPTURE) && not board[premove.to]:
+ return
+ var san = chess.__move_to_san(premove,chess.__generate_moves({legal = true}),false)
+ if san:
+ var legal_moves = chess.moves({square = chess.algebraic(premove.from), stripped = true})
+ var is_possible_move = legal_moves.find(chess.stripped_san(san)) != -1
+ if chess.__move_from_san(san, true) and (is_possible_move): # it is valid
+ Log.debug(["Executing premove:", san])
+ move(san, true, false) # make the move, send it to the opponent, dont prompt for premoves
+ premove = {}
+ else:
+ set_take_move_circle_color(premove_color)
SaveLoad.save("user://game.json", {pgn = chess.pgn(), fen = chess.fen()})
clear_last_clicked()
check_game_over()
diff --git a/ui/board/Board.tscn b/ui/board/Board.tscn
index 41abe0d..1e11add 100644
--- a/ui/board/Board.tscn
+++ b/ui/board/Board.tscn
@@ -18,6 +18,13 @@ script = ExtResource( 1 )
__meta__ = {
"_edit_group_": true
}
+overlay_color = Color( 0.0784314, 0.333333, 0.117647, 0.498039 )
+premove_color = Color( 0.55575, 0.455, 0.65, 0.784314 )
+last_move_indicator_color = Color( 0.74902, 0.662745, 0.223529, 0.498039 )
+last_move_take_indicator = Color( 0.74902, 0.407843, 0.223529, 0.498039 )
+clockrunning_color = Color( 0.219608, 0.278431, 0.133333, 1 )
+clockrunninglow = Color( 0.47451, 0.172549, 0.164706, 1 )
+clocklow = Color( 0.313726, 0.156863, 0.14902, 1 )
[node name="Background" type="GridContainer" parent="."]
margin_right = 600.0
@@ -48,6 +55,7 @@ usage = 0
render_target_update_mode = 3
[node name="Arrows" type="Control" parent="Canvas/Viewport"]
+unique_name_in_owner = true
script = ExtResource( 2 )
red_overlay = Color( 0.729412, 0.254902, 0.254902, 1 )
green_overlay = Color( 0.1272, 0.53, 0.18762, 1 )
diff --git a/ui/menus/tests/engine_test.gd b/ui/menus/tests/engine_test.gd
index 25704c3..bf12d76 100644
--- a/ui/menus/tests/engine_test.gd
+++ b/ui/menus/tests/engine_test.gd
@@ -1,8 +1,9 @@
extends Button
+class_name TestButton
class TestChess:
- extends Resource
+ extends Reference
const LOG_FILE = "user://tests.log"
@@ -10,7 +11,7 @@ class TestChess:
for k in Chess.SQUARE_MAP:
assert(Chess.algebraic(Chess.SQUARE_MAP[k]) == k)
- func test_perft():
+ func test_perf():
var perfts = [
{fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", depth = 3},
{fen = "8/PPP4k/8/8/8/8/4Kppp/8 w - - 0 1", depth = 4, nodes = 84923},
@@ -21,6 +22,16 @@ class TestChess:
var c = Chess.new(perft.fen)
var _nodes = c.perft(perft.depth)
+ func test_piece_move_generation():
+ var c = Chess.new()
+ c.load_pgn("1. e4 d5 2. f3 dxe4")
+ c.move("h4")
+ var m = c.localize_piece_move(c.piece_moves("e4", "p", c.turn, false)[1])
+ var san = c.__move_to_san(m)
+ var m2 = c.__move_from_san(san)
+ assert(m2.hash() == m.hash())
+ c.__make_move(m2)
+
func test_single_square_move_generation():
var positions = [
{
@@ -255,8 +266,10 @@ class TestChess:
SaveLoad.save_string("user://tests.log", "") #overwrite last logs
Log.file(LOG_FILE, "starting algebraic conversion tests")
test_algebraic_conversion()
- Log.file(LOG_FILE, "starting perft tests")
- test_perft()
+ Log.file(LOG_FILE, "starting performance tests")
+ test_perf()
+ Log.file(LOG_FILE, "starting piece move generation tests")
+ test_piece_move_generation()
Log.file(LOG_FILE, "starting move generation tests")
test_single_square_move_generation()
Log.file(LOG_FILE, "starting checkmate tests")
diff --git a/ui/menus/tests/tests.gd b/ui/menus/tests/tests.gd
index 247da6d..9eabe09 100644
--- a/ui/menus/tests/tests.gd
+++ b/ui/menus/tests/tests.gd
@@ -11,17 +11,14 @@ func _ready():
func _load(i: int):
+ PacketHandler.load_pgn(pgns[i])
in_sim = true
- var boar = load("res://ui/board/Game.tscn").instance()
- get_tree().get_root().add_child(boar)
- boar = boar.get_board()
- boar.load_pgn(pgns[i])
- get_parent().hide()
func _input(_event):
if Input.is_action_pressed("ui_cancel") and in_sim:
in_sim = false
+ PacketHandler.go_back("", true)
get_node("/root/Game").queue_free()
get_parent().show()
Globals.reset_vars()
diff --git a/ui/theme/main.theme b/ui/theme/main.theme
index de9bb9e..de7e127 100644
--- a/ui/theme/main.theme
+++ b/ui/theme/main.theme
Binary files differ