online multiplayer chess game (note server currently down)
Diffstat (limited to 'Board.gd')
| -rw-r--r-- | Board.gd | 616 |
1 files changed, 229 insertions, 387 deletions
@@ -1,9 +1,14 @@ extends Control class_name Grid -const PieceScene := preload("res://Piece.tscn") +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 remove_last + const piece_size := Vector2(80, 80) export(Color) var overlay_color := Color(0.078431, 0.333333, 0.117647, 0.498039) @@ -11,96 +16,116 @@ 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) -var matrix := [] -var stop_input := true -var background_array := [] -var last_clicked: Piece = null -var flipped := false +var board = [] # has `get_piece(algebraic position)` and `set_piece(algebraic position)` for ease of use -signal move_decided -var labels := {letters = [], numbers = []} +func get_piece(alg: String) -> Piece: + return board[Chess.SQUARE_MAP[alg]] -onready var background := $Background -onready var ASSETS_PATH: String = "res://assets/pieces/%s/" % Globals.piece_set + +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 = null +var last_clicked_moves := [] + +export(NodePath) var sidebar_path = @"" +onready var sidebar := get_node_or_null(sidebar_path) +export(NodePath) var ui_path = @"" +onready var darken = $Darken +onready var ui := get_node_or_null(ui_path) onready var foreground := $Foreground +onready var background := $Background onready var pieces := $Pieces -export(NodePath) onready var ui = get_node(ui) -export(NodePath) onready var sidebar = get_node(sidebar) -enum { PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING } +var chess := Chess.new() -func _init() -> void: +func _init(): Globals.grid = self -func _on_game_over(_reason: String, _isok: bool): - stop_input = true +func _exit_tree(): + Globals.grid = null -func _ready() -> void: - Events.connect("game_over", self, "_on_game_over") - rect_pivot_offset = rect_size / 2 - PacketHandler.connect("move_data", self, "play_san") - init_board() # create the tile squares - init_matrix() # create 2d matrix - init_pieces() # create the pieces - init_labels() # add the labels - Events.connect("turn_over", self, "_on_turn_over") # listen for turn_over events - Events.connect("outoftime", self, "_on_outoftime") # listen for timeout events +func _ready(): + Events.connect("turn_over", self, "_on_turn_over") + PacketHandler.connect("move_data", self, "move") + rect_min_size = piece_size * 8 + rect_pivot_offset = rect_min_size / 2 + board.resize(128) + init_board() + init_labels() + create_pieces() - Debug.monitor(self, "last_clicked") - stop_input = false +func init_board() -> 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.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 + background_array[i] = square # add the square to the background array -func _exit_tree() -> void: - Globals.grid = null # reset the globals grid when leaving tree +func init_labels() -> void: + foreground.offset = rect_global_position + for i in range(8): + labels.letters.append(init_label(i, Vector2(i, 7), "abcdefgh"[i], Vector2(10, -10), Label.VALIGN_BOTTOM)) + labels.numbers.append(init_label(i, Vector2(7, i), str(8 - i), Vector2(-10, 10), 0, Label.VALIGN_BOTTOM)) -func _input(event: InputEvent) -> void: # input - if event.is_action_released("debug"): # if debug - print_matrix_pretty(matrix) # print the matrix +func init_label(i: int, position: Vector2, text: String, off := Vector2.ZERO, valign := 0, align := 0) -> Label: + var label := Label.new() + label.rect_size = piece_size + label.align = align + label.valign = valign + label.rect_position = (position * piece_size) + off + label.text = text + label.add_color_override("font_color", Globals.board_color1 if i % 2 == 0 else Globals.board_color2) + var font: DynamicFont = load("res://ui/ubuntu-bold.tres").duplicate() + font.size = 15 + label.add_font_override("font", font) + foreground.add_child(label) + return label -static func print_matrix_pretty(mat: Array) -> void: # print the matrix - var topper_header := "┏━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┳━━━┓" - var middle_header := "┣━━━╋━━━╋━━━╋━━━╋━━━╋━━━╋━━━╋━━━╋━━━┫" - var middish_heads := "┗━━━╋━━━╋━━━╋━━━╋━━━╋━━━╋━━━╋━━━╋━━━┫" - var smaller_heads := " ┗━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻━━━┛" - var letter_header := " ┃ a ┃ b ┃ c ┃ d ┃ e ┃ f ┃ g ┃ h ┃" - var ender := " ┃ " # for pretty prints - for j in range(8): # for each row - var r: Array = mat[j] # get the row - if j == 0: - print(topper_header) # print the top border - else: - print(middle_header) # print the middle border - var row := "%s %s%s" % [ender.strip_edges(), 8 - j, ender] # init the string - for i in range(8): # for each column - if typeof(r[i]) != TYPE_STRING: - var c: Piece = r[i] # get the column - row += "%s%s" % [c.mininame, ender] if c else " " + ender # add the piece - else: - row += "%s%s" % [r[i] if r[i] else " ", ender] - print(row) # print the string - print("%s\n%s\n%s" % [middish_heads, letter_header, smaller_heads]) +func create_pieces(): + for k in Chess.SQUARE_MAP: + var piece = chess.get(k) + if piece: + make_piece(k, piece.type, piece.color) -func reload_sprites() -> void: - for i in range(8): - for j in range(8): - if matrix[i][j]: - matrix[i][j].load_texture() + +func make_piece(algebraic: String, piece_type: String, color := "w") -> void: # make peace + var piece := PieceScene.instance() # create a piece + var position = Chess.algebraic2vec(algebraic) # get the position + piece.name = "%s@%s" % [piece_type, algebraic] + piece.position = algebraic + piece.type = piece_type + piece.rect_global_position = position * piece_size # set the global position + piece.rect_min_size = piece_size + piece.rect_pivot_offset = piece_size / 2 # rotate around center + piece.color = color + pieces.add_child(piece) # add the piece to the grid + set_piece(algebraic, piece) func flip_pieces() -> void: - for i in range(8): - for j in range(8): - var spot: Piece = matrix[i][j] - if spot: - spot.sprite.flip_v = flipped - spot.sprite.flip_h = flipped + 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: @@ -112,10 +137,6 @@ func flip_labels() -> void: letlabel.text = "hgfedcba"[number - 1] -func flip_panels() -> void: - pass - - func flip_board() -> void: sidebar.flip_panels() if flipped: @@ -130,338 +151,159 @@ func flip_board() -> void: flip_labels() -func init_labels() -> void: - foreground.offset = rect_global_position - for i in range(8): - labels.letters.append( - init_label(i, Vector2(i, 7), "abcdefgh"[i], VALIGN_BOTTOM, Label.ALIGN_LEFT, Vector2(10, -10)) - ) - labels.numbers.append( - init_label(i, Vector2(7, i), str(8 - i), VALIGN_TOP, Label.ALIGN_RIGHT, Vector2(-10, 10)) - ) +func square_clicked(clicked_square: String) -> void: + if chess.turn != Globals.team or Globals.spectating: + return + var p := get_piece(clicked_square) + if !p or p.color != Globals.team: + if !is_instance_valid(last_clicked): + return + for m in last_clicked_moves: + if m.to == clicked_square: + move(m.san, false) + break + clear_circles() + + elif last_clicked != p: + if is_instance_valid(last_clicked): + clear_circles() + last_clicked = p + p.background.show() + var movs = chess.moves({"square": clicked_square, "verbose": true}) + for mov in movs: + if "c" in mov.flags: + get_piece(mov.to).frame.show() + else: + background_array[Chess.SQUARE_MAP[mov.to]].circle.show() + #e.p && castling dont really need attention here + last_clicked_moves.append(mov) + + +func move(san: String, is_recieved_move := true) -> void: + if is_valid_move(san): + var sound_handled = false + var move_0x88 = chess.__move_from_san(san, true) + 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)) + var _rook := 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)) + var _rook := 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 = board[move_0x88.from].move(Chess.algebraic(move_0x88.to)) + if !is_recieved_move: # was my turn, this is my move + yield(p.tween, "tween_all_completed") + p.open_promotion_previews() + yield(p, "promotion_decided") + move_0x88["promotion"] = p.promote_to + PacketHandler.send_mov(chess.__move_to_san(move_0x88)) # we changed "promotion", so send update the san + p.queue_free() + else: # was opponents turn, this is opponents move: promotion is already chosen + p.queue_free() # the q_f above happens after a dozen yields + # 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: + var _p = board[move_0x88.from].move(Chess.algebraic(move_0x88.to)) + if !is_recieved_move: + PacketHandler.send_mov(san) # move may have been modified, so recreat the san + if !sound_handled: + SoundFx.play("Move") + emit_signal("add_to_pgn", san) + Events.emit_signal("turn_over") + else: + Log.err("move %s is invalid!" % san) -func init_label(i: int, position: Vector2, text: String, valign := 0, align := 0, off := Vector2.ZERO) -> Label: - var label := Label.new() - label.rect_size = piece_size - label.align = align - label.valign = valign - label.rect_position = (position * piece_size) + off - label.text = text - label.add_color_override("font_color", Globals.board_color1 if i % 2 == 0 else Globals.board_color2) - var font: DynamicFont = load("res://ui/ubuntu-bold.tres").duplicate() - font.size = 15 - label.add_font_override("font", font) - foreground.add_child(label) - return label +func is_valid_move(san: String) -> bool: + var movs = chess.moves() + for mov in movs: + if mov == san: + return true + return false + + +func clear_circles(): + darken.hide() + if not last_clicked: + return + last_clicked.background.hide() + for move in last_clicked_moves: + if ("c" in move.flags or "e" in move.flags) and get_piece(move.to): # the take may have been used as the move, so this may just do nothing. on enpasant + get_piece(move.to).frame.hide() # for the take circle + background_array[Chess.SQUARE_MAP[move.to]].circle.hide() + last_clicked_moves = [] + last_clicked = null + +func clear_pieces() -> void: + for i in Chess.SQUARE_MAP.values(): + var p = board[i] + if p: + p.queue_free() + board[i] = null -func drawed(reason := "") -> void: + +func draw(reason := "") -> void: var string = "draw by " + reason ui.set_status(string, 0) Events.emit_signal("game_over", string, true) - SoundFx.play("Draw") + SoundFx.play("Victory") yield(get_tree().create_timer(5), "timeout") Events.emit_signal("go_back", string, true) -func win(winner: bool, reason := "") -> void: - var string = "%s won the game by %s" % ["white" if winner else "black", reason] +func win(winner: String, reason := "") -> void: + var string = "%s won the game by %s" % [Utils.expand_color(winner), reason] ui.set_status(string, 0) #: black won the game by checkmate Events.emit_signal("game_over", string, true) - Log.info([string, Utils.get_pgn()]) SoundFx.play("Victory") yield(get_tree().create_timer(5), "timeout") Events.emit_signal("go_back", string, true) -func check_in_check(prin := false) -> bool: # check if in_check - for i in range(0, 8): # for each row - for j in range(0, 8): # for each column - var spot: Piece = matrix[i][j] # get the square - if spot and spot.white != Globals.turn: # enemie - if spot.can_attack_piece(Globals.white_king if Globals.turn else Globals.black_king): # if it can take the king - if prin: - # control never flows here - Globals.in_check = true # set in_check - Globals.checking_piece = spot # set checking_piece - SoundFx.play("Check") - Log.debug("check") - return true # stop at the first check found - return false - - -func can_move() -> bool: - for i in range(0, 8): # for each row - for j in range(0, 8): # for each column - var spot: Piece = matrix[i][j] # get the square - if spot and spot.white == Globals.turn: - if spot.can_move(): - Log.debug("%s %s can mov!" % [Globals.get_turn(), spot.shortname]) - return true - - Log.debug("can no mov!") - return false - - -func init_matrix() -> void: # create the matrix - for i in range(8): # for each row - matrix.append([]) # add a row - for _j in range(8): # for each column - matrix[i].append(null) # add a square - - -func make_piece(position: Vector2, piece_type: int, white: bool = true, visible: bool = true) -> void: # make peace - var piece := PieceScene.instance() # create a piece - piece.name = Utils.to_str(piece_type) - piece.script = load("res://pieces/%s.gd" % Utils.to_str(piece_type)) # set the script - piece.real_position = position # set the real position - piece.visible = visible - piece.rect_global_position = position * piece_size # set the global position - piece.white = white # set its team - pieces.add_child(piece) # add the piece to the grid - matrix[position.y][position.x] = piece - - -func init_board() -> void: # create the board - for x in range(8): - for y in range(8): # for each column - var square := Square.instance() # create a square - square.name = Utils.to_algebraic(Vector2(y, x)) - square.hint_tooltip = square.name - square.rect_min_size = piece_size # set the size - square.color = Globals.board_color1 if (x + y) % 2 == 0 else Globals.board_color2 # set the color - background.add_child(square) # add the square to the background - square.connect("clicked", self, "square_clicked", [Vector2(y, x)]) # connect the clicked event - background_array.append(square) # add the square to the background array - - -func get_background_element(pos: Vector2) -> ColorRect: - return background_array[8 * pos.y + pos.x] - - -func init_pieces(visible: bool = true) -> void: # add the pieces - load_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", visible) - - -func check_for_circle(position: Vector2) -> bool: # check for a circle, validating movement - return get_background_element(position).circle_on - - -func check_for_frame(position: Vector2) -> bool: # check for a frame, validating taking - if !is_instance_valid(matrix[position.y][position.x]): # if there is no piece - return false # there is no frame - return matrix[position.y][position.x].frameon # return if the frame is on - - -func square_clicked(position: Vector2) -> void: # square clicked - if stop_input or Globals.turn != Globals.team or Globals.spectating: - return - Log.debug(Utils.to_algebraic(position) + " clicked") - var spot: Piece = matrix[position.y][position.x] # get the spot - if !spot or spot.white != Globals.team: - if !is_instance_valid(last_clicked): - return - if check_for_frame(position): # takeable - handle_take(position) - stop_input = true - emit_signal("move_decided") - elif check_for_circle(position): # see if theres a circle at the position - handle_move(position) # move - stop_input = true - emit_signal("move_decided") - if last_clicked: - last_clicked.clear_clicked() # remove the circles - last_clicked = null # set it to null - elif last_clicked != spot: # we got a new piece (or pawn) clicked - if is_instance_valid(last_clicked): # remove the circles - last_clicked.clear_clicked() - last_clicked = spot # set it to the new spot - spot.clicked() # tell the piece shit happeend - - -func handle_take(position: Vector2) -> void: - if Utils.is_pawn(last_clicked): # if its a pawn - if check_promote(last_clicked, position, "take"): - return - var mov = Move.new(SanParse.from_str(last_clicked.shortname), [last_clicked.real_position, position], true) - PacketHandler.send_mov(mov) # piece taking piece - - -func handle_move(position: Vector2) -> void: - if Utils.is_king(last_clicked) and last_clicked.can_castle: - for i in range(len(last_clicked.can_castle)): - var castle_data = last_clicked.can_castle[i] - if castle_data[0] == position: - # send some packet - var mov = Move.new(SanParser.KING, castle_data[3]) - PacketHandler.send_mov(mov) - return - if Utils.is_pawn(last_clicked): - var pawn: Pawn = last_clicked - if pawn.enpassant: - for i in range(len(pawn.enpassant)): - var en_passant_data = pawn.enpassant[i] - if en_passant_data[0] == position: - # send some packet - var mov = Move.new(SanParser.PAWN, [pawn.real_position, position], true) - PacketHandler.send_mov(mov) - return - elif check_promote(pawn, position): - return - var mov = Move.new(SanParse.from_str(last_clicked.shortname), [last_clicked.real_position, position]) - PacketHandler.send_mov(mov) - - -func check_promote(pawn, position, calltype: String = "move") -> bool: - if pawn.can_promote(position): - pawn.promote(position, calltype) - return true - return false - - -func clear_fx() -> void: # clear the circles - for i in range(8): # for each row - for j in range(8): # for each column - var square: ColorRect = get_background_element(Vector2(i, j)) # get the square - square.set_circle(false) # set the circle to false - var piece: Piece = matrix[i][j] # get the piece - if piece: # if there is a piece - piece.set_frame(false) # clear the frame - $Darken.hide() - - -func _on_outoftime(who: bool) -> void: - win(who, "time") - - -func _on_turn_over() -> void: - Globals.checking_piece = null # reset checking_piece - Globals.in_check = false # reset in_check - check_in_check(true) # check if in_check - if !can_move(): - if Globals.in_check: - win(!Globals.turn, "checkmate") - else: - drawed("stalemate") - - -func play_pgn(pgn: String, instant := false): - var hitlist = [] - if instant: - kill_matrix() - hitlist = pieces.get_children() - else: - kill_pieces() - stop_input = true - init_pieces(!instant) # if instant, hide the pieces - var mvtext = Pgn.parse(pgn).moves - for san in mvtext: - play_san(san, instant) # instant is not working right right now - # so just change the delay :> - if instant: - yield(get_tree(), "idle_frame") - else: - yield(get_tree().create_timer(.3), "timeout") - for i in range(8): - for j in range(8): - if matrix[i][j]: - matrix[i][j].update_visual_position() - matrix[i][j].show() - for c in hitlist: - if is_instance_valid(c): - c.free() - stop_input = false - - -func play_san(san: String, instant := false, set_input := true) -> void: - Log.debug("playing " + san) - var san_to_add := san - var mov = SanParse.parse(san).make_long() - Globals.add_turn() - match mov.move_kind.type: - Move.MoveKind.CASTLE: - var side = 0 if Globals.turn else 7 - var rook: Rook - var rook_goto: Vector2 - var kingpos = Vector2(4, side) - var king: King = Piece.at_pos(kingpos) - var king_goto: Vector2 - match mov.move_kind.data: - Move.MoveKind.CASTLETYPES.KING_SIDE: - rook = Piece.at_pos(Vector2(7, side)) - rook_goto = Vector2(5, side) - king_goto = Vector2(6, side) - Move.MoveKind.CASTLETYPES.QUEEN_SIDE: - rook = Piece.at_pos(Vector2(0, side)) - rook_goto = Vector2(3, side) - king_goto = Vector2(2, side) - rook.moveto(rook_goto, instant) - king.castle(king_goto, instant) - Move.MoveKind.NORMAL: - # this handles promotion, taking, enpassant, and moves. - var positions = mov.move_kind.data - if mov.promotion != -1: # promotion part - Piece.at_pos(positions[0]).promote_to(mov.promotion, mov.is_capture, positions[1], instant) - - elif mov.is_capture: # taking part - Globals.reset_halfmove() - if Piece.at_pos(positions[1]): - Piece.at_pos(positions[0]).take(Piece.at_pos(positions[1]), instant) - elif mov.piece == SanParser.PAWN: # enpassant part - var pawn: Pawn = Piece.at_pos(positions[0]) - pawn.passant(positions[1], instant) - san_to_add += " e.p." - else: # a very normal move - var piece = Piece.at_pos(positions[0]) - piece.moveto(positions[1], instant) - Globals.reset_halfmove() - Utils.add_move(san_to_add) - stop_input = false if set_input else stop_input - - -func load_fen(fen: String, visible: bool = true): - var data: Dictionary = Fen.parse(fen) - load_matrix(data.mat, visible) - Globals.turn = data.turn - Globals.fullmove = data.fullmove - Globals.halfmove = data.halfmove - - -func load_matrix(mat: Array, visible: bool = true): - if visible: - kill_pieces() - for x in range(8): - for y in range(8): - var ret = from_str_with_team(mat[y][x]) - if ret[0] != -1: - make_piece(Vector2(x, y), ret[0], ret[1], visible) - - -func from_str_with_team(string: String) -> Array: - var result = SanParser.from_str(string) - if result != -1: - return [result, true] - result = SanParser.from_str(string.to_upper()) - return [result, false] - - -func kill_pieces(): - for i in pieces.get_children(): - i.free() - kill_matrix() - - -func kill_matrix(): - matrix = [] - init_matrix() - - -func undo(last_pgn := Utils.pop_move()): - Globals.turn = true - Globals.fullmove = 1 - Globals.halfmove = 0 - Globals.in_check = false - Globals.checking_piece = null - clear_fx() - play_pgn(last_pgn, true) +func load_pgn(pgn: String) -> void: + chess.load_pgn(pgn, {sloppy = true}) + clear_pieces() + create_pieces() + emit_signal("clear_pgn") + var movs = Pgn.parse(pgn).moves + for mov in movs: + emit_signal("add_to_pgn", mov) + + +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_circles() + create_pieces() + + +func _on_turn_over(): + if chess.in_checkmate(): + # they won if its my turn, i won if its their turn. + win(Globals.team if Globals.team != chess.turn else Chess.__swap_color(Globals.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") |