online multiplayer chess game (note server currently down)
premove support :sparkles:
closes #14
| -rw-r--r-- | Globals.gd | 7 | ||||
| -rw-r--r-- | Square.gd | 57 | ||||
| -rw-r--r-- | Square.tscn | 40 | ||||
| -rw-r--r-- | Utils.gd | 40 | ||||
| -rw-r--r-- | assets/silhouette/B.png | bin | 655 -> 561 bytes | |||
| -rw-r--r-- | assets/silhouette/K.png | bin | 651 -> 566 bytes | |||
| -rw-r--r-- | assets/silhouette/N.png | bin | 764 -> 628 bytes | |||
| -rw-r--r-- | assets/silhouette/P.png | bin | 556 -> 497 bytes | |||
| -rw-r--r-- | assets/silhouette/Q.png | bin | 1188 -> 802 bytes | |||
| -rw-r--r-- | assets/silhouette/R.png | bin | 564 -> 395 bytes | |||
| -rw-r--r-- | board/chess.gd | 151 | ||||
| -rw-r--r-- | networking/PacketHandler.gd | 3 | ||||
| -rw-r--r-- | piece/Piece.gd | 63 | ||||
| -rw-r--r-- | piece/check-circle.tres | 2 | ||||
| -rw-r--r-- | piece/move-circle.tres | 13 | ||||
| -rw-r--r-- | piece/takeable-circle.tres | 4 | ||||
| -rw-r--r-- | project.godot | 10 | ||||
| -rw-r--r-- | ui/board/Board.gd | 143 | ||||
| -rw-r--r-- | ui/board/Board.tscn | 8 | ||||
| -rw-r--r-- | ui/menus/tests/engine_test.gd | 21 | ||||
| -rw-r--r-- | ui/menus/tests/tests.gd | 7 | ||||
| -rw-r--r-- | ui/theme/main.theme | bin | 1977 -> 1943 bytes |
22 files changed, 356 insertions, 213 deletions
@@ -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: @@ -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"] @@ -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 Binary files differindex 6f4826c..7de8773 100644 --- a/assets/silhouette/B.png +++ b/assets/silhouette/B.png diff --git a/assets/silhouette/K.png b/assets/silhouette/K.png Binary files differindex 2f0a7df..512d162 100644 --- a/assets/silhouette/K.png +++ b/assets/silhouette/K.png diff --git a/assets/silhouette/N.png b/assets/silhouette/N.png Binary files differindex 3a2a021..037f77c 100644 --- a/assets/silhouette/N.png +++ b/assets/silhouette/N.png diff --git a/assets/silhouette/P.png b/assets/silhouette/P.png Binary files differindex 0497bd4..e19f5ea 100644 --- a/assets/silhouette/P.png +++ b/assets/silhouette/P.png diff --git a/assets/silhouette/Q.png b/assets/silhouette/Q.png Binary files differindex 3ff64e1..e5e36c7 100644 --- a/assets/silhouette/Q.png +++ b/assets/silhouette/Q.png diff --git a/assets/silhouette/R.png b/assets/silhouette/R.png Binary files differindex 328525f..b950502 100644 --- a/assets/silhouette/R.png +++ b/assets/silhouette/R.png 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 Binary files differindex de9bb9e..de7e127 100644 --- a/ui/theme/main.theme +++ b/ui/theme/main.theme |