online multiplayer chess game (note server currently down)
Diffstat (limited to 'ui/board/Board.gd')
| -rw-r--r-- | ui/board/Board.gd | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/ui/board/Board.gd b/ui/board/Board.gd new file mode 100644 index 0000000..a72b4f3 --- /dev/null +++ b/ui/board/Board.gd @@ -0,0 +1,471 @@ +extends Control +class_name Grid + +const PieceScene := preload("res://piece/Piece.tscn") +const Square := preload("res://Square.tscn") + +### for the sandisplay +signal add_to_pgn(move) +signal clear_pgn +signal load_pgn(moves) +signal remove_last + +var move_indicators: PoolIntArray = [] + +var rot: float = 0 +var piece_size: Vector2 + +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 + + +func get_piece(alg: String) -> Piece: + return board[Chess.SQUARE_MAP[alg]] + + +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: 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 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() +var local := false +var spectating := false +var team: String +var auto_change_team := false + + +func _init(): + Globals.grid = self + + +func _exit_tree(): + Globals.grid = null + + +func _process(_delta): + rect_rotation = rot + foreground.rect_rotation = rot + if Input.is_action_just_pressed("debug") and Debug.debug: + print(chess.ascii()) + + +func _resized(): + var old_pc = piece_size + piece_size = rect_size / 8 + piece_size.x = clamp(piece_size.x, 0, piece_size.y) + piece_size.y = clamp(piece_size.y, 0, piece_size.x) + check_circle.width = piece_size.x + 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 + if not (board.empty() && background_array.empty()) && piece_size != old_pc: + resize_board() + 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(): + set_take_move_circle_color() + _resized() + Events.connect("turn_over", self, "_on_turn_over") + PacketHandler.connect("move_data", self, "move", [false, false]) + create_pieces() + create_squares() + create_labels() + yield(get_tree(), "idle_frame") + if !team: + team = chess.turn + auto_change_team = true + Log.debug("board: ready") + + +func resize_board(): + resize_pieces() + + +func create_squares() -> void: # create the board + background_array.resize(128) + for i in Chess.SQUARE_MAP.values(): + var alg := Chess.algebraic(i) + var square := Square.instance() # create a square + square.name = alg + square.b = self + square.square = alg + 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", [square]) # connect the clicked event + background_array[i] = square # add the square to the background array + arrows._setup(self) # initialize the arrows + + +func create_labels() -> void: + var font: DynamicFont = load("res://ui/ubuntu-bold-regular.tres").duplicate() + font.size = 15 + for k in Chess.SQUARE_MAP: + if k == "h1": + var l = init_label(font, k, k[0], VALIGN_BOTTOM, 0, false) + var n = init_label(font, k, k[1], 0, VALIGN_BOTTOM, false) + var h = HBoxContainer.new() + h.mouse_filter = MOUSE_FILTER_IGNORE + for i in [l, n]: + var ic = create_margin_container() + ic.add_child(i) + h.add_child(ic) + labels.numbers.append(n) + labels.letters.append(l) + foreground.add_child(h) + elif k[0] == "h": # file h contains numbers + labels.numbers.append(init_label(font, k, k[1], 0, VALIGN_BOTTOM)) + elif k[1] == "1": # rank 1 contains letters + labels.letters.append(init_label(font, k, k[0], VALIGN_BOTTOM)) + else: + var spacer = Control.new() + spacer.mouse_filter = MOUSE_FILTER_IGNORE + spacer.name = k + "_space" + spacer.size_flags_horizontal = SIZE_EXPAND_FILL + spacer.size_flags_vertical = SIZE_EXPAND_FILL + foreground.add_child(spacer) + + +func init_label(font: DynamicFont, alg: String, text: String, valign := 0, align := 0, add := true) -> Label: + var label := Label.new() + label.align = align + label.valign = valign + label.size_flags_horizontal = SIZE_EXPAND_FILL + label.size_flags_vertical = SIZE_EXPAND_FILL + label.mouse_filter = MOUSE_FILTER_IGNORE + label.name = text + label.text = text + label.add_color_override( + "font_color", Globals.board_color1 if Chess.square_color(alg) == "dark" else Globals.board_color2 + ) + label.add_font_override("font", font) + if add: + var container := create_margin_container() + container.add_child(label) + foreground.add_child(container) + return label + + +func create_margin_container(margin := 5) -> MarginContainer: + var c := MarginContainer.new() + c.add_constant_override("margin_top", margin) + c.add_constant_override("margin_left", margin) + c.add_constant_override("margin_right", margin) + c.add_constant_override("margin_bottom", margin) + c.size_flags_horizontal = SIZE_EXPAND_FILL + c.size_flags_vertical = SIZE_EXPAND_FILL + c.mouse_filter = MOUSE_FILTER_IGNORE + return c + + +func clear_pieces() -> void: + for i in Chess.SQUARE_MAP.values(): + var p: Piece = board[i] + if p: + p.queue_free() + board[i] = null + + +func resize_pieces(): + for i in Chess.SQUARE_MAP.values(): + var p: Piece = board[i] + if p: + p.size() + + +func create_pieces(): + board.resize(128) + for k in Chess.SQUARE_MAP: + var piece = chess.get(k) + if piece: + make_piece(k, piece.type, piece.color) + + +func make_piece(algebraic: String, piece_type: String, color := "w") -> void: # make peace + var piece := PieceScene.instance() # create a piece + piece.name = "%s-%s" % [piece_type, algebraic] + piece.b = self + piece.position = algebraic + piece.type = piece_type + piece.color = color + pieces.add_child(piece) # add the piece to the grid + set_piece(algebraic, piece) + + +func flip_pieces() -> void: + for i in Chess.SQUARE_MAP.values(): + var spot: Piece = board[i] + if spot: + spot.sprite.flip_v = flipped + spot.sprite.flip_h = flipped + + +func flip_labels() -> void: + for i in range(8): + var numlabel: Label = labels.numbers[i] + var letlabel: Label = labels.letters[i] + var number := i + 1 if flipped else 8 - i + numlabel.text = str(number) + letlabel.text = "hgfedcba"[number - 1] + + +func flip_board() -> void: + rot = 0 if rot == 180 else 180 + flipped = rot == 180 + Log.debug(["Flipped the board, now", "flipped" if flipped else "not flipped"]) + if sidebar: + sidebar.flip_panels() + flip_pieces() + flip_labels() + + +func is_my_turn() -> bool: + return team == chess.turn + + +func square_clicked(clicked_square: BackgroundSquare) -> void: + if Globals.spectating: + return + + var p := get_piece(clicked_square.square) + + if not is_my_turn() 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, 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 != 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.square && m.from == last_clicked.position: + move(m.san) + clear_last_clicked() + return + + clear_last_clicked() + + if p and p.color == team: + if chess.turn != team: + if !Globals.premoves: + return + clicked_square.show_premove_indicators() + else: + clicked_square.show_move_indicators() + last_clicked = p + + +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}) + if valid_moves.find(chess.stripped_san(san)) == -1: + Log.err("Invalid move " + san) + return + Log.debug("Making move " + san) + chess.__make_move(move_0x88) + if move_0x88.flags & Chess.BITS.CAPTURE: + board[move_0x88.to].took() + SoundFx.play("Capture") + sound_handled = true + elif move_0x88.flags & Chess.BITS.EP_CAPTURE: + var to_take := Chess.offset(move_0x88.to, Vector2(0, 1 * -1 if chess.turn == Chess.WHITE else 1)) + get_piece(to_take).took() + SoundFx.play("Capture") + sound_handled = true + elif move_0x88.flags & Chess.BITS.KSIDE_CASTLE: # kingside castling + var rook_pos := Chess.offset(move_0x88.to, Vector2(1, 0)) + get_piece(rook_pos).move(Chess.offset(move_0x88.to, Vector2(-1, 0))) + elif move_0x88.flags & Chess.BITS.QSIDE_CASTLE: # queenside + var rook_pos := Chess.offset(move_0x88.to, Vector2(-2, 0)) + 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 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 + p.queue_free() + else: # was opponents turn, this is opponents move: promotion is already chosen + p.queue_free() + 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 send && !local: + PacketHandler.send_mov(san) + if !sound_handled: + SoundFx.play("Move") + emit_signal("add_to_pgn", san) + Events.emit_signal("turn_over") + + +func clear_last_clicked(): + last_clicked = null + + +func draw(reason := "") -> void: + var string = "draw by " + reason + game.set_status(string, 0) + SoundFx.play("Victory") + Events.emit_signal("game_over", string) + + +func win(winner: String, reason := "") -> void: + var string = "%s won the game by %s" % [Utils.expand_color(winner), reason] + game.set_status(string, 0) #: black won the game by checkmate + Events.emit_signal("game_over", string) + SoundFx.play("Victory") + + +func load_pgn(pgn: String) -> void: + chess.load_pgn(pgn, {sloppy = true}) + clear_pieces() + create_pieces() + emit_signal("clear_pgn") + var pgn_parser := PGN.new() + var movs: PoolStringArray = pgn_parser.parse(pgn).moves + emit_signal("load_pgn", movs) + Log.info("load pgn " + pgn) + Events.emit_signal("turn_over") + + +func undo(two: bool = false) -> void: + Globals.chat.server("undid move %s" % chess.undo().san) + emit_signal("remove_last") + if two: + Globals.chat.server("undid move %s" % chess.undo().san) + emit_signal("remove_last") + clear_pieces() + clear_last_clicked() + create_pieces() + Events.emit_signal("turn_over") + + +func auto_flip(): + if team == Chess.WHITE and flipped: + flip_board() + elif team == Chess.BLACK and not flipped: + flip_board() + + +func _on_turn_over(): + # if auto_change_team: + # team = chess.turn + # auto_flip() + + if is_my_turn(): + 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 = {} + elif Globals.premoves: + set_take_move_circle_color(premove_color) + SaveLoad.save("user://game.json", {pgn = chess.pgn(), fen = chess.fen()}) + clear_last_clicked() + check_game_over() + create_last_move_indicators() + + +func check_game_over(): + if chess.in_checkmate(): + # they won if its my turn, i won if its their turn. + win(team if is_my_turn() else Chess.__swap_color(team), "checkmate") + elif chess.half_moves >= 50: + draw("fifty move rule") + elif chess.in_stalemate(): + draw("stalemate") + elif chess.insufficient_material(): + draw("insufficient material") + elif chess.in_threefold_repetition(): + draw("threefold repetition") + + +func create_last_move_indicators(): + for i in move_indicators: + background_array[i].move_indicator.color = last_move_indicator_color + background_array[i].move_indicator.hide() + if !chess.__history: + return + var m: Dictionary = chess.__history[-1].move + background_array[m.from].move_indicator.show() + if m.flags & Chess.BITS.CAPTURE: + background_array[m.to].move_indicator.color = last_move_take_indicator + background_array[m.to].move_indicator.show() + move_indicators = [m.from, m.to] + + +func reload(): + premove = {} + chess = Chess.new() + clear_last_clicked() + clear_pieces() + create_pieces() + create_last_move_indicators() # it hides the indicators :/ + Events.emit_signal("turn_over") + emit_signal("clear_pgn") # clears the san displays |