online multiplayer chess game (note server currently down)
better engine (#11)
52 files changed, 2082 insertions, 1848 deletions
@@ -2,5 +2,6 @@ logs/ *.sh *.py +*.pgn .vscode/ exports/ @@ -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") @@ -2,9 +2,6 @@ extends Node # warning-ignore-all:unused_signal signal turn_over -signal just_before_turn_over # called just before turn over -signal outoftime # called when the time is up signal game_over(reason, isok) # called when the game is over signal go_back(reason, isok) # called when the game is over, and were ready to go back -signal data_recieved # called every time the data comes in signal set_signed_in(signed_in) @@ -1,12 +1,12 @@ extends Node class_name FEN +var reg = Utils.compile( + "^(?<pieces>([pnbrqkPNBRQK1-8]{1,8}/?){8})\\s+(?<turn>b|w)\\s+(?<castling>-|K?Q?k?q?)\\s+(?<enpassant>-|[a-h][3-6])\\s+(?<halfmove>\\d+)\\s+(?<fullmove>\\d+)" +) + func parse(fen: String) -> Dictionary: - var reg = RegEx.new() - reg.compile( - "^(?<pieces>([pnbrqkPNBRQK1-8]{1,8}\\/?){8})\\s+(?<turn>b|w)\\s+(?<castling>-|K?Q?k?q)\\s+(?<enpassant>-|[a-h][3-6])\\s+(?<halfmove>\\d+)\\s+(?<fullmove>\\d+)\\s*$" - ) var res = reg.search(fen) if res: var mat: Array = [] @@ -22,7 +22,7 @@ func parse(fen: String) -> Dictionary: mat.append(append_row) var fenobj = { "mat": mat, - "turn": res.strings[res.names.turn] == "w", + "turn": res.strings[res.names.turn], "castling": res.strings[res.names.castling], "enpassant": res.strings[res.names.enpassant], "halfmove": int(res.strings[res.names.halfmove]), @@ -32,48 +32,3 @@ func parse(fen: String) -> Dictionary: else: Log.err("bad fen") return {} - - -func get_fen() -> String: - var pieces := "" - for rank in range(8): - var empty := 0 - for file in range(8): - var spot: Piece = Globals.grid.matrix[rank][file] - if spot == null: - empty += 1 - if len(pieces) > 0 and str(empty - 1) == pieces[-1]: - pieces[-1] = str(empty) - else: - pieces += str(empty) - else: - pieces += (spot.shortname[0].to_upper() if spot.white else spot.shortname[0].to_lower()) - empty = 0 - if rank != 7: - pieces += "/" - # handle castling checks - var whitecastling := PoolStringArray(Globals.white_king.castleing(true)).join("") - var blackcastling := PoolStringArray(Globals.black_king.castleing(true)).join("") - var castlingrights := "" - if blackcastling and whitecastling: - castlingrights = whitecastling.to_upper() + blackcastling.to_lower() - else: - castlingrights = "-" - # castling rights are slightly janke - - var enpassants := "" - for pawn in Globals.pawns: - if pawn.just_double_stepped and pawn.just_set: - enpassants = Utils.to_algebraic(pawn.real_position + (Vector2.DOWN * pawn.whiteint)) - break - return ( - "%s %s %s %s %s %s" - % [ - pieces, - "w" if Globals.turn else "b", - castlingrights, - enpassants if enpassants else "-", - Globals.halfmove, - Globals.fullmove, - ] - ) # pos # turn # castling # enpassant # halfmove # fullmove @@ -3,8 +3,8 @@ extends Control onready var status: StatusLabel = find_node("Status") onready var sidebar := $Holder/SidebarRight onready var panels := [ - sidebar.blackpanel, sidebar.whitepanel, + sidebar.blackpanel, ] @@ -21,15 +21,16 @@ func get_board() -> Node: func _spectate_info(info: Dictionary) -> void: - var whitepnl = panels[int(Globals.WHITE)] + var whitepnl = panels[0] set_panel(whitepnl, info.white.name, info.white.country) - var blackpnl = panels[int(Globals.BLACK)] + var blackpnl = panels[1] set_panel(blackpnl, info.black.name, info.black.country) func _on_info(info: Dictionary) -> void: - set_panel(panels[int(!Globals.team)], info.name, info.country) # enemy panel - set_panel(panels[int(Globals.team)], SaveLoad.get_data("id").name, SaveLoad.get_data("id").country) # own panel + var enemy_int = 1 if Globals.team == "w" else 0 + set_panel(panels[enemy_int], info.name, info.country) # enemy panel + set_panel(panels[abs(enemy_int - 1)], SaveLoad.get_data("id").name, SaveLoad.get_data("id").country) # own panel func set_panel(pnl, name, country) -> void: @@ -227,8 +227,8 @@ anchor_right = 0.0 anchor_bottom = 0.0 margin_right = 640.0 margin_bottom = 640.0 -ui = NodePath("../../..") -sidebar = NodePath("../../SidebarRight") +sidebar_path = NodePath("../../SidebarRight") +ui_path = NodePath("../../..") [node name="Chat" parent="Holder/middle" instance=ExtResource( 9 )] anchor_right = 0.0 @@ -1,80 +1,24 @@ extends Node -const WHITE = true -const BLACK = false - -var __nosethalfmove := false -var pawns := [] # PoolPawnArray -var team := true -var grid: Grid = null +var team := "w" var piece_set := "california" -var fullmove := 1 -var halfmove := 0 -var in_check := false -var checking_piece: Piece = null var board_color1: Color = Color(0.870588, 0.890196, 0.901961) var board_color2: Color = Color(0.54902, 0.635294, 0.678431) -var white_king: King = null -var black_king: King = null var spectating := false var chat: Chat = null -var turn := true # true for white, false for black -# true cuz white goes first +var grid: Grid = null func reset_vars() -> void: - __nosethalfmove = false - pawns = [] - team = true - spectating = false + team = "w" grid = null - fullmove = 1 - halfmove = 0 - in_check = false - checking_piece = null - white_king = null - black_king = null - turn = true - Utils.reset_vars() - - -func reset_halfmove() -> void: - halfmove = 0 - __nosethalfmove = true - - -func add_turn() -> void: - Events.emit_signal("just_before_turn_over") - turn = not turn - if turn: # white just moved - fullmove += 1 - if __nosethalfmove: - __nosethalfmove = false - else: - halfmove += 1 - Events.emit_signal("turn_over") - - -func str_bool(b: bool) -> String: - return "white" if b else "black" - - -func get_turn() -> String: - return str_bool(turn) - - -func get_team() -> String: - return str_bool(team) + chat = null + spectating = false func _ready() -> void: Log.info("startup") VisualServer.set_default_clear_color(Color.black) - Debug.monitor(self, "fullmove") - Debug.monitor(self, "halfmove") - Debug.monitor(self, "in_check") - Debug.monitor(self, "turn", "get_turn()") - Debug.monitor(self, "team", "get_team()") Debug.monitor(self, "static memory", "get_static_memory()") Debug.monitor(self, "dynamic memory", "get_dynamic_memory()") Debug.monitor(Engine, "fps", "get_frames_per_second()") @@ -1,20 +1,19 @@ extends Node class_name PGN +var movetextex = Utils.compile( + "([NBKRQ]?[a-h]?[1-8]?[\\-x]?[a-h][1-8](?:=?[nbrqkNBRQK])?|[PNBRQK]?@[a-h][1-8]|--|Z0|0000|@@@@|O-O(?:-O)?|0-0(?:-0)?)|(\\{.*)|(;.*)|(\\$[0-9]+)|(\\()|(\\))|(\\*|1-0|0-1|1\\/2-1\\/2)|([\\?!]{1,2})" +) +var tagex = Utils.compile('^\\[([A-Za-z0-9_]+)\\s+"([^\\r]*)"\\]\\s*$') +var tagnameex = Utils.compile("^[A-Za-z0-9_]+\\Z") -func parse(pgn: String, tags := true): + +func parse(pgn: String, tags := true) -> Dictionary: # put tags into a dictionary, # and the moves into a array - - var movetextex = SanParse.compile( - "([NBKRQ]?[a-h]?[1-8]?[\\-x]?[a-h][1-8](?:=?[nbrqkNBRQK])?|[PNBRQK]?@[a-h][1-8]|--|Z0|0000|@@@@|O-O(?:-O)?|0-0(?:-0)?)|(\\{.*)|(;.*)|(\\$[0-9]+)|(\\()|(\\))|(\\*|1-0|0-1|1\\/2-1\\/2)|([\\?!]{1,2})", - false - ) var lines = Array(pgn.split("\n")) var headers := {} if tags: - var tagex = SanParse.compile('^\\[([A-Za-z0-9_]+)\\s+"([^\\r]*)"\\]\\s*$', false) - var tagnameex = SanParse.compile("^[A-Za-z0-9_]+\\Z", false) # get headers while !lines.empty(): var line = lines[0].strip_edges() @@ -33,7 +32,7 @@ func parse(pgn: String, tags := true): headers[cap[1]] = cap[2] else: # invalid headers - return null + return {} else: break var movetext := PoolStringArray() diff --git a/SanParse/Move.gd b/SanParse/Move.gd deleted file mode 100644 index 66fbbd7..0000000 --- a/SanParse/Move.gd +++ /dev/null @@ -1,124 +0,0 @@ -class_name Move -extends Resource - -enum CHECKTYPES { NONE, CHECK, CHECKMATE } -var generated_from := "" -var piece := -1 -var move_kind: MoveKind -var promotion := -1 -# var annotation := "" # later -var check_type := 0 -var is_capture := false - - -func _init(newpiece: int, newmove, capture := false) -> void: - piece = newpiece - is_capture = capture - move_kind = MoveKind.new(newmove) - - -static func castle_type(type: String) -> int: - return MoveKind.CASTLETYPES.QUEEN_SIDE if type == "O-O-O" else MoveKind.CASTLETYPES.KING_SIDE - - -func set_check_type(type: String) -> Move: - match type: - "+": - check_type = CHECKTYPES.CHECK - "#": - check_type = CHECKTYPES.CHECKMATE - _: - check_type = CHECKTYPES.NONE - return self - - -func compile() -> String: # compiles the structure to a san - var res := "" - match move_kind.type: - MoveKind.CASTLE: - res += move_kind.to_str() - MoveKind.NORMAL: - res += to_str(piece) - res += Utils.to_algebraic(move_kind.data[0]) - res += "x" if is_capture else "" - res += Utils.to_algebraic(move_kind.data[1]) - res += "=" + to_str(promotion) if promotion != -1 else "" - match check_type: - CHECKTYPES.CHECK: - res += "+" - CHECKTYPES.CHECKMATE: - res += "#" - return res.strip_edges() - - -static func to_str(type: int) -> String: - return " NBRQK"[type].strip_edges() # if its a pawn, return nothing - - -### fix short san -func make_long() -> Move: - if move_kind.type == MoveKind.CASTLE: - return self - var newvecs: PoolVector2Array = [] - - var vectors = move_kind.data - - if Piece.is_on_board(vectors[0]): # [0] is the only one with -1(s) possible - return self - - if is_capture: - newvecs.append(long_helper(vectors[0], true, false, vectors[1])) - else: - newvecs.append(long_helper(vectors[0], false, true, vectors[1])) - - if newvecs.empty(): - Log.error("cruddlesticks") - return self - newvecs.append(vectors[1]) - - move_kind.data = newvecs - return self - - -func long_helper(vec: Vector2, attack: bool, move: bool, touch: Vector2): - if vec.y == -1 and vec.x != -1: - for y in range(8): - var spot = Piece.at_pos(Vector2(vec.x, y)) - if long_helper_helper(spot, touch, attack, move): - return Vector2(vec.x, y) - elif vec.x == -1 and vec.y != -1: - for x in range(8): - var spot = Piece.at_pos(Vector2(x, vec.y)) - if long_helper_helper(spot, touch, attack, move): - return Vector2(x, vec.y) - elif vec == Vector2(-1, -1): - for x in range(8): - for y in range(8): - var spot = Piece.at_pos(Vector2(x, y)) - if long_helper_helper(spot, touch, attack, move): - return Vector2(x, y) - - -func long_helper_helper(spot, touch, attack, move): - return Utils.spotispiece(piece, spot) and spot.white == Globals.turn and spot.can_touch(touch, attack, move) - - -class MoveKind: - extends Resource - enum CASTLETYPES { NONE, QUEEN_SIDE, KING_SIDE } - enum { NONE, NORMAL, CASTLE } - var type := 0 - var data # string OR array - - func _init(something): - if typeof(something) == TYPE_ARRAY and len(something) == 2: - type = NORMAL - data = PoolVector2Array(something) - elif typeof(something) == TYPE_INT: - type = CASTLE - data = something - else: - assert(false) - - func to_str() -> String: - return "O-O-O" if data == CASTLETYPES.QUEEN_SIDE else "O-O" diff --git a/SanParse/SanParse.gd b/SanParse/SanParse.gd deleted file mode 100644 index 53663af..0000000 --- a/SanParse/SanParse.gd +++ /dev/null @@ -1,163 +0,0 @@ -extends Node -class_name SanParser - -const end := "(\\+|\\#)?(\\?\\?|\\?|\\?!|!|!!)?$" # annotation - -var regexs := { - "pawn_move": compile("^([a-h])([1-8])"), - "long_pawn_move": compile("^([a-h])([1-8])([a-h])([1-8])"), # long-san - "piece_movement": compile("^([KQBNR])([a-h])([1-8])"), - "specific_row_piece_movement": compile("^([KQBNR])([0-9])([a-h])([1-8])"), - "specific_column_piece_movement": compile("^([KQBNR])([a-h])([a-h])([1-8])"), - "long_piece_movement": compile("^([KQBNR])([a-h])([0-9])([a-h])([1-8])"), - "pawn_capture": compile("^([a-h])x([a-h])([1-8])(?:=?([KQBNR]))?"), - "long_pawn_capture": compile("^([a-h])([1-8])x([a-h])([1-8])(?:=?([KQBNR]))?"), - "piece_capture": compile("^([KQBNR])x([a-h])([1-8])"), - "specific_column_piece_capture": compile("^([KQBNR])([a-h])x([a-h])([1-8])"), - "specific_row_piece_capture": compile("^([KQBNR])([0-9])x([a-h])([1-8])"), - "long_piece_capture": compile("^([KQBNR])([a-h])([0-9])x([a-h])([1-8])"), - "pawn_promotion": compile("^([a-h])([1-8])=?([KQBNR])"), - "long_pawn_promotion": compile("^([a-h])([1-8])([a-h])([1-8])=?([KQBNR])"), - "castling": compile("^(O-O-O|O-O)"), -} - - -static func pos(col: String, row: String) -> Vector2: - return Utils.from_algebraic(col + row) - - -const UNKNOWN_POS = Vector2(-1, -1) -enum { PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING } - - -static func from_str(string: String) -> int: - var find = " NBRQK".find(string) - if find != -1: - return find - else: - return "PNBRQK".find(string) - - -func compile(regxstr: String, app_end := true) -> RegEx: #app_end because append end get it - var reg = RegEx.new() - reg.compile(regxstr + end if app_end else regxstr) - return reg - - -func parse(san: String) -> Move: - var mv = regexmatch(san) - mv.generated_from = san # for debugging i just moved the thing over so i can do this - return mv - - -func regexmatch(san: String) -> Move: - var re: RegExMatch = regexs.pawn_move.search(san) - if re: - var cap = re.strings - var mov = Move.new(PAWN, [UNKNOWN_POS, pos(cap[1], cap[2])]).set_check_type(cap[3]) - return mov - - re = regexs.long_pawn_move.search(san) - if re: - var cap = re.strings - var mov = Move.new(PAWN, [pos(cap[1], cap[2]), pos(cap[3], cap[4])]).set_check_type(cap[5]) - return mov - - re = regexs.piece_movement.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [UNKNOWN_POS, pos(cap[2], cap[3])]) - mov.set_check_type(cap[4]) - return mov - - re = regexs.specific_row_piece_movement.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [Vector2(-1, Utils.row_pos(cap[2])), pos(cap[3], cap[4])]) - mov.set_check_type(cap[5]) - return mov - - re = regexs.specific_column_piece_movement.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [Vector2(Utils.col_pos(cap[2]), -1), pos(cap[3], cap[4])]) - mov.set_check_type(cap[5]) - return mov - - re = regexs.long_piece_movement.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [pos(cap[2], cap[3]), pos(cap[4], cap[5])]) - mov.set_check_type(cap[6]) - return mov - - re = regexs.pawn_capture.search(san) - if re: - var cap = re.strings - var mov = Move.new(PAWN, [Vector2(Utils.col_pos(cap[1]), -1), pos(cap[2], cap[3])], true) - mov.promotion = from_str(cap[4]) - mov.set_check_type(cap[5]) - return mov - - re = regexs.long_pawn_capture.search(san) - if re: - var cap = re.strings - var mov = Move.new(PAWN, [pos(cap[1], cap[2]), pos(cap[3], cap[4])], true) - mov.promotion = from_str(cap[5]) - mov.set_check_type(cap[6]) - return mov - - re = regexs.piece_capture.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [UNKNOWN_POS, pos(cap[2], cap[3])], true) - mov.set_check_type(cap[4]) - return mov - - re = regexs.specific_column_piece_capture.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [Vector2(Utils.col_pos(cap[2]), -1), pos(cap[3], cap[4])], true) - mov.set_check_type(cap[5]) - return mov - - re = regexs.specific_row_piece_capture.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [Vector2(-1, Utils.row_pos(cap[2])), pos(cap[3], cap[4])], true) - mov.set_check_type(cap[5]) - return mov - - re = regexs.long_piece_capture.search(san) - if re: - var cap = re.strings - var mov = Move.new(from_str(cap[1]), [pos(cap[2], cap[3]), pos(cap[4], cap[5])], true) - mov.set_check_type(cap[6]) - return mov - - re = regexs.pawn_promotion.search(san) - if re: - var cap = re.strings - var mov = Move.new(PAWN, [UNKNOWN_POS, pos(cap[1], cap[2])], true) - mov.promotion = from_str(cap[3]) - mov.set_check_type(cap[4]) - return mov - - re = regexs.long_pawn_promotion.search(san) - if re: - var cap = re.strings - var mov = Move.new(PAWN, [pos(cap[1], cap[2]), pos(cap[3], cap[4])], true) - mov.promotion = from_str(cap[5]) - mov.set_check_type(cap[6]) - return mov - - re = regexs.castling.search(san) - if re: - var cap = re.strings - var mov = Move.new(KING, Move.castle_type(cap[1])) - mov.set_check_type(cap[2]) - return mov - - push_error("regex exhausted: no matches(%s)" % san) - - return null @@ -1,9 +1,8 @@ extends ColorRect +class_name BackgroundSquare signal clicked -var circle_on := false - onready var circle := $CircleHolder/Circle @@ -12,18 +11,12 @@ func _ready() -> void: circle.material.set_shader_param("color", Globals.grid.overlay_color) circle.visible = false rect_min_size = Globals.grid.piece_size - rect_size = rect_min_size if Globals.spectating: mouse_default_cursor_shape = CURSOR_FORBIDDEN else: mouse_default_cursor_shape = CURSOR_POINTING_HAND -func set_circle(boolean: bool) -> void: - circle_on = boolean - circle.visible = boolean - - func _gui_input(event: InputEvent): if !Globals.spectating and event is InputEventMouseButton and Input.is_action_pressed("click"): emit_signal("clicked") @@ -1,47 +1,20 @@ extends Node var internet := false -signal newmove(move) -signal newfen(fen) -signal pop_move(fen, was_num) -var moves_list: PoolStringArray = [] -var fen := "" - -func get_pgn(): - return moves_list.join(" ") - - -func _on_turn_over() -> void: - fen = Fen.get_fen() - Log.info("fen: " + fen) - emit_signal("newfen", fen) - SaveLoad.save_dict("user://game.json", {"fen": fen, "pgn": get_pgn()}, true) - - -func pop_move() -> String: - emit_signal("pop_move") - moves_list.remove(moves_list.size() - 1) - var pgn = get_pgn() - moves_list.resize(0) - return pgn - - -func spotispiece(piece_type: int, spot: Piece) -> bool: - return SanParse.from_str(spot.shortname.to_upper()) == piece_type if spot else false +static func compile(src: String) -> RegEx: + var regex := RegEx.new() + regex.compile(src) + return regex static func str_bool(string: String) -> bool: return string.to_lower().strip_edges() in ["true", "1", "on", "yes", "y", ""] -func add_move(move: String) -> void: - if Globals.turn == false: - moves_list.append("%s. %s" % [Globals.fullmove, move]) - else: - moves_list.append(move) - emit_signal("newmove", move) +func expand_color(color: String) -> String: + return "white" if color == "w" else "black" func get_args() -> Dictionary: @@ -56,14 +29,11 @@ func get_args() -> Dictionary: func _ready() -> void: - Events.connect("turn_over", self, "_on_turn_over") if "help" in get_args(): print("usage: ./chess%s [debug | help]" % exec_ext()) print("run with command debug to enable debug mode") print("run with command help to show this help") get_tree().quit() # dont wait - Debug.monitor(self, "fen") - Debug.monitor(self, "pgn", "get_pgn()") var t = Timer.new() add_child(t) t.name = "t" @@ -88,40 +58,11 @@ static func exec_ext() -> String: return "" -static func is_pawn(inode) -> bool: - return inode is Pawn - - -static func is_king(inode) -> bool: - return inode is King - - -func reset_vars() -> void: - moves_list.resize(0) - - -static func get_node_name(node: Node) -> Array: - if is_pawn(node): - return ["♙", "P"] if node.white else ["♟", "P"] - elif node is King: - return ["♔", "K"] if node.white else ["♚", "K"] - elif node is Queen: - return ["♕", "Q"] if node.white else ["♛", "Q"] - elif node is Rook: - return ["♖", "R"] if node.white else ["♜", "R"] - elif node is Bishop: - return ["♗", "B"] if node.white else ["♝", "B"] - elif node is Knight: - return ["♘", "N"] if node.white else ["♞", "N"] - else: - return ["", ""] - - func request() -> int: # returns err var http := HTTPRequest.new() add_child(http) var httpurl: String = PacketHandler.url.replace("wss://", "http://") - var error := http.request(httpurl, [], true, HTTPClient.METHOD_POST) + var error := http.request(httpurl, [], true, HTTPClient.METHOD_GET) http.free() internet = error == OK return error @@ -157,12 +98,6 @@ func _notification(what: int) -> void: Log.debug("Bye!") -static func to_algebraic(pos: Vector2) -> String: - var column = "abcdefgh"[pos.x] if pos.x != -1 else "" - var row = str(round(8 - pos.y)) if pos.y != -1 else "" - return column + row - - static func col_pos(col: String) -> int: return "abcdefgh".find(col) diff --git a/board/chess.gd b/board/chess.gd new file mode 100644 index 0000000..77a74bd --- /dev/null +++ b/board/chess.gd @@ -0,0 +1,1205 @@ +extends Node +class_name Chess +# ported from https://github.com/jhlywa/chess.js +const SYMBOLS := "pnbrqkPNBRQK" + +const DEFAULT_POSITION := "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + +const TERMINATION_MARKERS := ["1-0", "0-1", "1/2-1/2", "*"] + +const PAWN_OFFSETS := { + b = [16, 32, 17, 15], + w = [-16, -32, -17, -15], +} + +const PIECE_OFFSETS := { + n = [-18, -33, -31, -14, 18, 33, 31, 14], + b = [-17, -15, 17, 15], + r = [-16, 1, 16, -1], + q = [-17, -16, -15, 1, 17, 16, 15, -1], + k = [-17, -16, -15, 1, 17, 16, 15, -1], +} + +var ATTACKS: PoolIntArray = [ + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0, + 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, + 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, + 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, + 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0, + 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0, + 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0, + 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0, + 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0, + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20 +] + +var RAYS: PoolIntArray = [ + 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, + 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, + 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, + 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0, + 0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0, + 0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0, + 0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0, + 0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0, + -15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17 +] + +const SQUARE_MAP := { + a8 = 0, b8 = 1, c8 = 2, d8 = 3, e8 = 4, f8 = 5, g8 = 6, h8 = 7, + a7 = 16, b7 = 17, c7 = 18, d7 = 19, e7 = 20, f7 = 21, g7 = 22, h7 = 23, + a6 = 32, b6 = 33, c6 = 34, d6 = 35, e6 = 36, f6 = 37, g6 = 38, h6 = 39, + a5 = 48, b5 = 49, c5 = 50, d5 = 51, e5 = 52, f5 = 53, g5 = 54, h5 = 55, + a4 = 64, b4 = 65, c4 = 66, d4 = 67, e4 = 68, f4 = 69, g4 = 70, h4 = 71, + a3 = 80, b3 = 81, c3 = 82, d3 = 83, e3 = 84, f3 = 85, g3 = 86, h3 = 87, + a2 = 96, b2 = 97, c2 = 98, d2 = 99, e2 = 100, f2 = 101, g2 = 102, h2 = 103, + a1 = 112, b1 = 113, c1 = 114, d1 = 115, e1 = 116, f1 = 117, g1 = 118, h1 = 119 +} + +const SHIFTS := {p = 0, n = 1, b = 2, r = 3, q = 4, k = 5} + +const BITS := { + NORMAL = 1, + CAPTURE = 2, + BIG_PAWN = 4, # 2 step pawn + EP_CAPTURE = 8, + PROMOTION = 16, + KSIDE_CASTLE = 32, + QSIDE_CASTLE = 64, +} + +const ROOKS := { + w = [ + {square = SQUARE_MAP.a1, flag = BITS.QSIDE_CASTLE}, + {square = SQUARE_MAP.h1, flag = BITS.KSIDE_CASTLE}, + ], + b = [ + {square = SQUARE_MAP.a8, flag = BITS.QSIDE_CASTLE}, + {square = SQUARE_MAP.h8, flag = BITS.KSIDE_CASTLE}, + ], +} +enum { RANK_8, RANK_7, RANK_6, RANK_5, RANK_4, RANK_3, RANK_2, RANK_1 } +enum { PARSER_STRICT, PARSER_SLOPPY } +const BLACK := "b" +const WHITE := "w" +const EMPTY := -1 +const PAWN := "p" +const KNIGHT := "n" +const BISHOP := "b" +const ROOK := "r" +const QUEEN := "q" +const KING := "k" +const FLAGS := { + NORMAL = "n", + CAPTURE = "c", + BIG_PAWN = "b", + EP_CAPTURE = "e", + PROMOTION = "p", + KSIDE_CASTLE = "k", + QSIDE_CASTLE = "q", +} + + +# parses all of the decorators out of a SAN string +static func stripped_san(move: String) -> String: + var reg := RegEx.new() + reg.compile("(\\+|\\#)?(\\?\\?|\\?|\\?!|!|!!)?$") + return reg.sub(move.replace("=", ""), "") + + +# this func is used to uniquely identify ambiguous moves +func get_disambiguator(move: Dictionary, moves: Array) -> String: + var from: int = move.from + var to: int = move.to + var piece: String = move.piece + + var ambiguities := 0 + var same_rank := 0 + var same_file := 0 + var ambig_piece: String + var ambig_to: int + var ambig_from: int + for i in range(moves.size()): + ambig_from = moves[i].from + ambig_to = moves[i].to + ambig_piece = moves[i].piece + + # if a move of the same piece type ends on the same to square, we'll + # need to add a disambiguator to the algebraic notation + if piece == ambig_piece && from != ambig_from && to == ambig_to: + ambiguities += 1 + + if rank(from) == rank(ambig_from): + same_rank += 1 + + if file(from) == file(ambig_from): + same_file += 1 + + if ambiguities > 0: + # if there exists a similar moving piece on the same rank and file as + # the move in question, use the square as the disambiguator + if same_rank > 0 && same_file > 0: + return algebraic(from) + elif same_file > 0: + # if the moving piece rests on the same file, use the rank symbol as the + # disambiguator + return algebraic(from)[1] + else: + # else use the file symbol + return algebraic(from)[0] + return "" + + +static func infer_piece_type(san: String) -> String: + var piece_type := san[0] + if piece_type >= "a" && piece_type <= "h": + return PAWN + piece_type = piece_type.to_lower() + if piece_type == "o": + return KING + return piece_type + + +### +### utility functions +### +static func rank(i: int) -> int: + return i >> 4 + + +static func file(i: int) -> int: + return i & 15 + + +static func vec2algebraic(pos: Vector2) -> String: + var column := "abcdefgh"[pos.x] + var row := str(round(8 - pos.y)) + return column + row + + +static func algebraic2vec(alg: String) -> Vector2: + return Vector2("abcdefgh".find(alg[0]), 8 - int(alg[1])) + + +static func algebraic(i: int) -> String: + var f := file(i) + var r := rank(i) + return "abcdefgh"[f] + "87654321"[r] + + +static func __swap_color(c: String) -> String: + return BLACK if c == WHITE else WHITE + + +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 "" + + +# begin main functions + +var board := [] # stores all the pieces +var kings := {w = EMPTY, b = EMPTY} # stores the square of the kings +var turn := WHITE # whose turn is it +var castling := {w = 0, b = 0} # castling abilities +var ep_square := EMPTY # current en passant square; will be `e3` if you make `e4` +var half_moves := 0 # halfmove counter +var fullmoves := 1 # fullmove counter +var __history := [] + + +func _init(fen := DEFAULT_POSITION) -> void: + load_fen(fen) + board.resize(128) + + +# removes everything on the board ( 8/8/8/8/8/8/8/8 w - - 0 1 ) +func clear() -> void: + board.resize(0) + board.resize(128) + kings = {w = EMPTY, b = EMPTY} + turn = WHITE + castling = {w = 0, b = 0} + ep_square = EMPTY + half_moves = 0 + fullmoves = 1 + __history = [] + + +# goes back to the default position +func reset() -> void: + load_fen(DEFAULT_POSITION) + + +# loads a FEN string. see `fen()` +# returns false in the event of a failure to parse the FEN string. +func load_fen(fen): + var parsed: Dictionary = Fen.parse(fen) + if !parsed: + return false + clear() + + for x in range(8): + for y in range(8): + var piece: String = parsed.mat[y][x] + if piece: + put( + {type = piece.to_lower(), color = WHITE if piece < "a" else BLACK}, + vec2algebraic(Vector2(x, y)) + ) + turn = parsed.turn + if "K" in parsed.castling: + castling.w |= BITS.KSIDE_CASTLE + if "Q" in parsed.castling: + castling.w |= BITS.QSIDE_CASTLE + if "k" in parsed.castling: + castling.b |= BITS.KSIDE_CASTLE + if "q" in parsed.castling: + castling.b |= BITS.QSIDE_CASTLE + + ep_square = SQUARE_MAP[parsed.enpassant] if parsed.enpassant != "-" else EMPTY + half_moves = parsed.halfmove + fullmoves = parsed.fullmove + return true + + +# returns the FEN string of the current position +func fen() -> String: + var empty := 0 + var pieces := "" + var i := 0 + while i < SQUARE_MAP.h1: + if board[i] == null: + empty += 1 + else: + if empty > 0: + pieces += str(empty) + empty = 0 + var piece: String = board[i].type + var color: String = board[i].color + pieces += piece.to_upper() if color == WHITE else piece.to_lower() + + if (i + 1) & 0x88: + if empty > 0: + pieces += str(empty) + + if i != SQUARE_MAP.h1: + pieces += "/" + + empty = 0 + i += 8 + i += 1 + + var cflags := "" + if castling[WHITE] & BITS.KSIDE_CASTLE: + cflags += "K" + if castling[WHITE] & BITS.QSIDE_CASTLE: + cflags += "Q" + if castling[BLACK] & BITS.KSIDE_CASTLE: + cflags += "k" + if castling[BLACK] & BITS.QSIDE_CASTLE: + cflags += "q" + cflags = cflags if cflags else "-" + var epflags := "-" if ep_square == EMPTY else algebraic(ep_square) + + return "%s %s %s %s %s %s" % [pieces, turn, cflags, epflags, half_moves, fullmoves] + + +# gets a square from the board +func get(square: String) -> Dictionary: + var piece = board[SQUARE_MAP[square]] + return {type = piece.type, color = piece.color} if piece else {} + + +# PUTs a piece object into the specified square on the board, returns OK on sucess +func put(piece: Dictionary, square: String) -> int: + # check for valid piece object + valid piece + valid square + if ( + !("type" in piece && "color" in piece) + or SYMBOLS.find(piece.type.to_lower()) == -1 + or not square in SQUARE_MAP + ): + return ERR_INVALID_DATA + + var sq: int = SQUARE_MAP[square] + + # only one king + if piece.type == KING && !(kings[piece.color] == EMPTY || kings[piece.color] == sq): + return ERR_ALREADY_EXISTS + + board[sq] = {type = piece.type, color = piece.color} + if piece.type == KING: + kings[piece.color] = sq + + return OK + + +func remove(square) -> Dictionary: + var piece := get(square) + if piece: + board[SQUARE_MAP[square]] = null + if piece && piece.type == KING: + kings[piece.color] = EMPTY + return piece + + +# warning-ignore:shadowed_variable +func __build_move(board: Array, from: int, to: int, flags: int, promotion := ""): + var move := { + color = turn, + from = from, + to = to, + flags = flags, + piece = board[from].type, + } + + if promotion: + move.flags |= BITS.PROMOTION + move.promotion = promotion + + 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 + var them := __swap_color(us) + var second_rank := {b = RANK_7, w = RANK_2} + + var first_sq: int = SQUARE_MAP.a8 + var last_sq: int = SQUARE_MAP.h1 + var single_square := false + + # legal moves? + var legal: bool = options.legal if "legal" in options else true + var piece_type: String = ( + options.piece.to_lower() + if "piece" in options and typeof(options.piece) == TYPE_STRING + else "-1" + ) + # generating moves for a single square? + if "square" in options: + if options.square in SQUARE_MAP: + last_sq = SQUARE_MAP[options.square] + first_sq = last_sq - 1 + single_square = true + else: + return [] + + var i := first_sq + while i < last_sq: + i += 1 + # are we off the edge of the board + if i & 0x88: + i += 7 + continue + var piece = board[i] + if piece == null || piece.color != us: + continue + + if piece.type == PAWN && (piece_type == "-1" || piece_type == PAWN): + # 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) + + # 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) + + # pawn captures + for j in range(2, 4): + var _square: int = i + PAWN_OFFSETS[us][j] + if _square & 0x88: + continue + + if board[_square] != null && board[_square].color == them: + __add_move(board, moves, i, _square, BITS.CAPTURE) + elif _square == ep_square: + __add_move(board, 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 + + while true: + square += offset + if square & 0x88: + break + + if board[square] == null: + __add_move(board, moves, i, square, BITS.NORMAL) + else: + if board[square].color == us: + break + __add_move(board, moves, i, square, BITS.CAPTURE) + break + + # break, if knight or king + if piece.type == "n" || piece.type == "k": + break + + # check for castling if: a) we're generating all moves, or b) we're doing + # single square move generation on the king's square + if piece_type == "-1" || piece_type == KING: + if !single_square || last_sq == kings[us]: + # king-side castling + if castling[us] & BITS.KSIDE_CASTLE: + var castling_from: int = kings[us] + var castling_to: int = castling_from + 2 + + if ( + board[castling_from + 1] == null + && board[castling_to] == null + && !__attacked(them, kings[us]) + && !__attacked(them, castling_from + 1) + && !__attacked(them, castling_to) + ): + __add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE) + + # queen-side castling + if castling[us] & BITS.QSIDE_CASTLE: + var castling_from: int = kings[us] + var castling_to := castling_from - 2 + + if ( + board[castling_from - 1] == null + && board[castling_from - 2] == null + && board[castling_from - 3] == null + && !__attacked(them, kings[us]) + && !__attacked(them, castling_from - 1) + && !__attacked(them, castling_to) + ): + __add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE) + + # return all pseudo-legal moves (this includes moves that allow the king + # to be captured) + if !legal: + return moves + + # filter out illegal moves + var legal_moves := [] + for move in moves: + __make_move(move) + if !__king_attacked(us): + legal_moves.append(move) + __undo_move() + + return legal_moves + + +# convert a move from 0x88 coordinates to Standard Algebraic Notation +# (SAN) +# +# @param {boolean} sloppy Use the sloppy SAN generator to work around over +# disambiguation bugs in Fritz and Chessbase. See below: +# +# r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4 +# 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned +# 4. ... Ne7 is technically the valid SAN +func __move_to_san(move, moves := __generate_moves({legal = true}), annotations := true) -> String: + var output := "" + + if move.flags & BITS.KSIDE_CASTLE: + output = "O-O" + elif move.flags & BITS.QSIDE_CASTLE: + output = "O-O-O" + else: + if move.piece != PAWN: + var disambiguator := get_disambiguator(move, moves) + output += move.piece.to_upper() + disambiguator + + if move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE): + if move.piece == PAWN: + output += algebraic(move.from)[0] + output += "x" + + output += algebraic(move.to) + + if move.flags & BITS.PROMOTION: + output += "=" + move.promotion.to_upper() + if annotations: + __make_move(move) + if in_check(): + if in_checkmate(): + output += "#" + else: + output += "+" + __undo_move() + return output + + +func __attacked(color: String, square: int): + var i := 0 + + while i < SQUARE_MAP.h1: + i += 1 + # did we run off the end of the board + if i & 0x88: + i += 7 + continue + + # if empty square or wrong color + if board[i] == null || board[i].color != color: + continue + + var piece: Dictionary = board[i] + var difference := i - square + var index := difference + 119 + + if ATTACKS[index] & (1 << SHIFTS[piece.type]): + if piece.type == PAWN: + if difference > 0: + if piece.color == WHITE: + return true + else: + if piece.color == BLACK: + return true + continue + + # if the piece is a knight or a king + if piece.type == "n" || piece.type == "k": + return true + + var offset := RAYS[index] + var j := i + offset + + var blocked := false + while j != square: + if board[j] != null: + blocked = true + break + j += offset + + if !blocked: + return true + return false + + +func __king_attacked(color): + return __attacked(__swap_color(color), kings[color]) + + +func in_check(): + return __king_attacked(turn) + + +func in_checkmate(): + return in_check() && __generate_moves().size() == 0 + + +func in_stalemate(): + return !in_check() && __generate_moves().size() == 0 + + +func insufficient_material(): + var pieces := {b = 0, k = 0, r = 0, q = 0, n = 0, p = 0} + var bishops := [] + var num_pieces := 0 + var sq_color := 0 + var i := 0 + while i < SQUARE_MAP.h1: + i += 1 + + sq_color = (sq_color + 1) % 2 + if i & 0x88: + i += 7 + continue + + var piece = board[i] + if piece: + pieces[piece.type] += 1 + if piece.type == BISHOP: + bishops.append(sq_color) + num_pieces += 1 + # k vs k + if num_pieces == 2: + return true + elif num_pieces == 3 && (pieces[BISHOP] == 1 || pieces[KNIGHT] == 1): + # k vs. kn .... or .... k vs. kb + return true + elif num_pieces == pieces[BISHOP] + 2: + # kb vs. kb where any number of bishops are all on the same color + var sum := 0 + var lent := bishops.size() + for b in bishops: + sum += b + if sum == 0 || sum == lent: # check if on same color + return true + return false + + +func in_threefold_repetition(): + # TODO: while this func is fine for casual use, a better + # implementation would use a Zobrist key (instead of FEN). the + # Zobrist key would be maintained in the __make_move/__undo_move funcs, + # avoiding the costly funcs that we do below. + var moves := [] + var positions := {} + var repetition := false + + while true: + var move: Dictionary = __undo_move() + if !move: + break + moves.append(move) + + while true: + # remove the last two fields in the FEN string, they're not needed + # when checking for draw by rep + if !repetition: + var fen := PoolStringArray(Array(fen().split(" ")).slice(0, 3)).join(" ") + + # has the position occurred three or more times + positions[fen] = positions[fen] + 1 if fen in positions else 1 + if positions[fen] >= 3: + repetition = true + + if !moves.size(): + break + __make_move(moves.pop_back()) + + return repetition + + +func __push(move): + __history.append( + { + move = move, + kings = {b = kings.b, w = kings.w}, + turn = turn, + castling = {b = castling.b, w = castling.w}, + ep_square = ep_square, + half_moves = half_moves, + fullmoves = fullmoves, + } + ) + + +func __make_move(move: Dictionary): + var us := turn + var them := __swap_color(us) + __push(move) + + board[move.to] = board[move.from] + board[move.from] = null + + # 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 + + # if pawn promotion, replace with new piece + if move.flags & BITS.PROMOTION: + board[move.to] = {type = move.promotion, color = us} + + # if we moved the king + if board[move.to].type == KING: + kings[board[move.to].color] = move.to + + # if we castled, move the rook next to the king + if move.flags & BITS.KSIDE_CASTLE: + var castling_to: int = move.to - 1 + var castling_from: int = move.to + 1 + board[castling_to] = board[castling_from] + board[castling_from] = null + elif move.flags & BITS.QSIDE_CASTLE: + var castling_to: int = move.to + 1 + var castling_from: int = move.to - 2 + board[castling_to] = board[castling_from] + board[castling_from] = null + + # turn off castling + castling[us] = 0 + + # turn off castling if we move a rook + if castling[us]: + for rook in ROOKS[us]: + if move.from == rook.square && castling[us] & rook.flag: + castling[us] ^= rook.flag + break + # turn off castling if we capture a rook + if castling[them]: + for rook in ROOKS[them]: + if move.to == rook.square && castling[them] & rook.flag: + castling[them] ^= rook.flag + 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 + + # 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 + + if turn == BLACK: + fullmoves += 1 + turn = __swap_color(turn) + + +func __undo_move() -> Dictionary: + var old = __history.pop_back() + if old == null: + return {} + + 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 + fullmoves = old.fullmoves + + var us := turn + var them := __swap_color(turn) + + 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} + + if move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE): + var castling_to + var castling_from + if move.flags & BITS.KSIDE_CASTLE: + castling_to = move.to + 1 + castling_from = move.to - 1 + elif move.flags & BITS.QSIDE_CASTLE: + castling_to = move.to - 2 + castling_from = move.to + 1 + + board[castling_to] = board[castling_from] + board[castling_from] = null + + return move + + +# convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates +func __move_from_san(move, sloppy := false) -> Dictionary: + # strip off any move decorations: e.g Nf3+?! becomes Nf3 + var clean_move := stripped_san(move) + + var overly_disambiguated := false + var piece + var from + var to + var promotion + var matches + + # the move parsers is a 2-step state + for parser in range(2): + if parser == PARSER_SLOPPY: + # only run the sloppy parse if explicitly requested + if !sloppy: + return {} + + # The sloppy parser allows the user to parse non-standard chess + # notations. This parser is opt-in (by specifying the + # '{ sloppy: true }' setting) and is only run after the Standard + # Algebraic Notation (SAN) parser has failed. + # + # When running the sloppy parser, we'll run a regex to grab the piece, + # the to/from square, and an optional promotion piece. This regex will + # parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7, + # f7f8q, b1c3 + # + # NOTE: Some positions and moves may be ambiguous when using the + # sloppy parser. For example, in this position: + # 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, the move b1c3 may be interpreted + # as Nc3 or B1c3 (a disambiguated bishop move). In these cases, the + # sloppy parser will default to the most most basic interpretation + # (which is b1c3 parsing to Nc3). + + var move_regex := RegEx.new() + move_regex.compile("([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])") + + # The [a-h]?[1-8]? portion of the regex below handles moves that may + # be overly disambiguated (e.g. Nge7 is unnecessary and non-standard + # when there is one legal knight move to e7). In this case, the value + # of 'from' variable will be a rank or file, not a square. + var fallback := RegEx.new() + fallback.compile("([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?") + + var result := move_regex.search(clean_move) + if result: + matches = result.strings + piece = matches[1] + from = matches[2] + to = matches[3] + promotion = matches[4] + else: + var _result := fallback.search(clean_move) + + if _result: + matches = _result.strings + piece = matches[1] + from = matches[2] + to = matches[3] + promotion = matches[4] + + if from.length() == 1: + overly_disambiguated = true + + var piece_type := infer_piece_type(clean_move) + var moves := __generate_moves( + { + legal = true, + piece = piece if piece else piece_type, + } + ) + for move in moves: + match parser: + PARSER_STRICT: + var m := stripped_san(__move_to_san(move, moves, false)) + if clean_move == m: + return move + continue + PARSER_SLOPPY: + if matches: + # hand-compare move properties with the results from our sloppy regex + if ( + (!piece || piece.to_lower() == move.piece) + && from in SQUARE_MAP + && to in SQUARE_MAP + && SQUARE_MAP[from] == move.from + && SQUARE_MAP[to] == move.to + && (!promotion || promotion.to_lower() == move.promotion) + ): + return move + elif overly_disambiguated: + # SPECIAL CASE: we parsed a move string that may have an + # unneeded rank/file disambiguator (e.g. Nge7). The 'from' + # variable will be validated + var square := algebraic(move.from) + if ( + (!piece || piece.to_lower() == move.piece) + && to in SQUARE_MAP + && SQUARE_MAP[to] == move.to + && (from == square[0] || from == square[1]) + && (!promotion || promotion.to_lower() == move.promotion) + ): + return move + return {} + + +func __make_pretty(ugly_move: Dictionary) -> Dictionary: + var move := ugly_move.duplicate() + move.san = __move_to_san(move, __generate_moves({legal = true})) + move.to = algebraic(move.to) + move.from = algebraic(move.from) + + var flags := "" + + for flag in BITS: + if BITS[flag] & move.flags: + flags += FLAGS[flag] + + move.flags = flags + return move + + +#***************************************************************************** +#* DEBUGGING UTILITIES +#****************************************************************************/ +func perft(depth: int) -> int: + var moves := __generate_moves({legal = false}) + var nodes := 0 + var color := turn + for move in moves: + __make_move(move) + if !__king_attacked(color): + if depth - 1 > 0: + var child_nodes := perft(depth - 1) + nodes += child_nodes + else: + nodes += 1 + __undo_move() + + return nodes + + +#*************************************************************************** +#* PUBLIC API +#*************************************************************************** + + +func in_draw(): + return half_moves >= 50 || in_stalemate() || insufficient_material() || in_threefold_repetition() + + +func game_over(): + return ( + half_moves >= 50 + || in_checkmate() + || in_stalemate() + || insufficient_material() + || in_threefold_repetition() + ) + + +# lists the possible legal moves. +func moves(options := {}): + # The internal representation of a chess move is in 0x88 format, and + # not meant to be human-readable. The code below converts the 0x88 + # square coordinates to algebraic coordinates. It also prunes an + # unnecessary move keys resulting from a verbose call. + + var ugly_moves := __generate_moves(options) + var moves := [] + for ugly_move in ugly_moves: + # does the user want a full move object (most likely not), or just + # SAN + if "verbose" in options && options.verbose: + moves.append(__make_pretty(ugly_move)) + else: + moves.append(__move_to_san(ugly_move, __generate_moves({legal = true}))) + return moves + + +# warning-ignore:function_conflicts_variable +# returns a 2d matrix of the board. +func board(): + var output := [] + var row := [] + var i := 0 + while i < SQUARE_MAP.h1: + i += 1 + if board[i] == null: + row.append(null) + else: + row.append( + { + square = algebraic(i), + type = board[i].type, + color = board[i].color, + } + ) + if (i + 1) & 0x88: + output.append(row) + row = [] + i += 8 + return output + + +func pgn() -> String: + # using the specification from http://www.chessclub.com/help/PGN-spec + # pop all of __history onto reversed_history + var reversed_history := [] + while !__history.empty(): + reversed_history.append(__undo_move()) + var moves: PoolStringArray = [] + var move_string := "" + # build the list of moves. a move_string looks like: "3. e3 e6" + while reversed_history.size() > 0: + var move: Dictionary = reversed_history.pop_back() + + # if the position started with black to move, start PGN with 1. ... + if !__history.size() and move.color == "b": + move_string = "%s ..." % fullmoves + elif move.color == "w": + if move_string.length() > 0: + moves.append(move_string) + move_string = "%s." % fullmoves + + move_string += " %s" % __move_to_san(move, __generate_moves({legal = true})) + __make_move(move) + + # __history should be back to what it was before we started generating PGN, + # so join together moves + return moves.join(" ") + + +func load_pgn(pgn, options := {}) -> int: # FIXME: mildly broken. move generator for a8 cannot get a4, for some reason. + # allow the user to specify the sloppy move parser to work around over + # disambiguation bugs in Fritz and Chessbase + var sloppy: bool = options.sloppy if "sloppy" in options else false + + var parsed: Dictionary = Pgn.parse(pgn) + + # Put the board in the starting position + reset() + + var fen := "" + + for key in parsed.headers: + # check to see user is including fen (possibly with wrong tag case) + if key.to_lower() == "fen": + fen = parsed.headers[key] + + # sloppy parser should attempt to load a fen tag, even if it's + # the wrong case and doesn't include a corresponding [SetUp "1"] tag */ + if sloppy and fen: + if !load_fen(fen): + return ERR_INVALID_DATA + else: + # strict parser - load_fen the starting position indicated by [Setup '1'] + # and [FEN position] + if "SetUp" in parsed.headers and parsed.headers["SetUp"] == "1": + if !("FEN" in parsed.headers && load_fen(parsed.headers["FEN"])): + return ERR_INVALID_DATA + + var moves: Array = parsed.moves + var move := {} + var result := "" + for mov in moves: + move = __move_from_san(mov, sloppy) + if !move: + # was the move an end of game marker + if TERMINATION_MARKERS.find(mov) != -1: + result = mov + else: + return ERR_INVALID_DATA + else: + # reset the end of game marker if making a valid move + result = "" + __make_move(move) + # Per section 8.2.6 of the PGN spec, the Result tag pair must match + # match the termination marker. Only do this when headers are present, + # but the result tag is missing + if result && parsed.headers.size() && !parsed.header["Result"]: + moves.append(result) + return OK + + +# The move func can be called with in the following parameters: +# +# .move('Nxb7') <- where 'move' is a case-sensitive SAN string +# +# .move({ from: 'h7', to :'h8', promotion: 'q'}) <- where the 'move' is a move obj +func move(move, sloppy := false) -> Dictionary: + var move_obj = null + + if typeof(move) == TYPE_STRING: + move_obj = __move_from_san(move, sloppy) + elif typeof(move) == TYPE_DICTIONARY: + var moves := __generate_moves() + + # uglify move + for m in moves: + if ( + move.from == algebraic(m.from) + && move.to == algebraic(m.to) + && (!("promotion" in m) || move.promotion == m.promotion) + ): + move_obj = m + break + + # failed to find move + if !move_obj: + return {} + + # need to make a copy of move because we can't generate SAN after the + # move is made + var pretty_move := __make_pretty(move_obj) + + __make_move(move_obj) + + return pretty_move + + +func undo() -> Dictionary: + var move := __undo_move() + return __make_pretty(move) if move else {} + + +func ascii() -> String: + var s := " +------------------------+\n" + var i := 0 + while i < SQUARE_MAP.h1 + 1: + # display the rank + if file(i) == 0: + s += " " + "87654321"[rank(i)] + " |" + + # empty piece + if board[i] == null: + s += " . " + else: + var piece: String = board[i].type + var color: String = board[i].color + var symbol := piece.to_upper() if color == WHITE else piece.to_lower() + s += " " + symbol + " " + if (i + 1) & 0x88: + s += "|\n" + i += 8 + i += 1 + s += " +------------------------+\n" + s += " a b c d e f g h" + + return s + + +static func square_color(square): + if square in SQUARE_MAP: + var sq_0x88: int = SQUARE_MAP[square] + return "light" if (rank(sq_0x88) + file(sq_0x88)) % 2 == 0 else "dark" + return null + + +func history(verbose := false) -> Array: + var reversed_history := [] + var move_history := [] + + while __history.size() > 0: + reversed_history.append(__undo_move()) + + while reversed_history.size() > 0: + var move: Dictionary = reversed_history.pop_back() + if verbose: + move_history.append(__make_pretty(move)) + else: + move_history.append(__move_to_san(move, __generate_moves({legal = true}))) + __make_move(move) + + return move_history diff --git a/networking/PacketHandler.gd b/networking/PacketHandler.gd index 18a4360..89a5cb6 100644 --- a/networking/PacketHandler.gd +++ b/networking/PacketHandler.gd @@ -89,15 +89,15 @@ func _data_recieved() -> void: Globals.spectating = true _start_game() yield(get_tree().create_timer(.5), "timeout") - Globals.grid.play_pgn(text.pgn, true) + Globals.grid.load_pgn(text.pgn) emit_signal("info_recieved", text) HEADERS.loadpgn: _start_game() yield(get_tree().create_timer(.5), "timeout") Log.info("load pgn " + text) - Globals.grid.play_pgn(text, true) # call deferred wont work since grid obj may be null + Globals.grid.load_pgn(text) # call deferred wont work since grid obj may be null HEADERS.stopgame: - if !leaving: # dont emit the signal if its a stophost thing (HACK) + if !leaving: # dont go back if its a stophost thing (HACK) go_back(text, true) leaving = false HEADERS.signal: @@ -134,7 +134,7 @@ func join_result(accepted) -> void: func flip_if_black(): yield(get_tree(), "idle_frame") - if Globals.team == Globals.BLACK: + if Globals.team == Chess.BLACK: Globals.grid.flip_board() @@ -146,7 +146,7 @@ func host_result(accepted) -> void: func handle_result(accepted, resultstring: String) -> bool: if typeof(accepted) == TYPE_DICTIONARY: - Globals.team = !bool(accepted.idx) + Globals.team = "w" if accepted.idx == 0 else "b" lobby.set_status(resultstring, true) return true lobby.set_status(accepted, false) @@ -159,10 +159,10 @@ func go_back(error: String, isok: bool) -> void: Globals.reset_vars() if has_node("/root/Game"): $"/root/Game".queue_free() - lobby.set_status(error, isok) - lobby.toggle(true) - lobby.focus() - lobby.set_buttons(true) + lobby.set_status(error, isok) + lobby.toggle(true) + lobby.focus() + lobby.set_buttons(true) func _start_game() -> void: @@ -194,11 +194,8 @@ func join_game(game: String = game_code) -> void: func host_game(game: String = game_code, white := true, moves_array: PoolStringArray = []) -> void: - send_gamecode_packet( - Utils.append_dict(SaveLoad.get_public_info(), {team = white, moves = moves_array}), - HEADERS.hostrequest, - game - ) + var pckt := Utils.append_dict(SaveLoad.get_public_info(), {team = white, moves = moves_array}) + send_gamecode_packet(pckt, HEADERS.hostrequest, game) func spectate(game: String = game_code) -> void: @@ -213,9 +210,9 @@ func relay_signal(body: Dictionary, header: String) -> Dictionary: # its really return signal(body, header, HEADERS.relay) -func send_mov(mov: Move): - send_packet({move = mov.compile(), gamecode = game_code}, HEADERS.move) +func send_mov(mov: String) -> void: + send_gamecode_packet({move = mov}, HEADERS.move) func stopgame(reason: String) -> void: - send_packet({"reason": reason, "gamecode": game_code}, HEADERS.stopgame) + send_gamecode_packet({"reason": reason}, HEADERS.stopgame) diff --git a/piece/Piece.gd b/piece/Piece.gd new file mode 100644 index 0000000..468c3b3 --- /dev/null +++ b/piece/Piece.gd @@ -0,0 +1,94 @@ +extends Control +class_name Piece + +var position := "" +var color := "w" +var type := "" +var tween := Tween.new() + +onready var sprite = $Sprite +onready var frame = $Sprite/Frame +onready var background = $ColorRect +onready var anim = $AnimationPlayer +onready var rotate = $RotatePlayer + +# for pawn promotion +var previews: VBoxContainer = null +var popup: PopupPanel = null +signal promotion_decided +var promote_to := "" + + +func _ready(): + add_child(tween) + load_texture() + frame.modulate = Globals.grid.overlay_color + background.color = Globals.grid.overlay_color + + sprite.flip_v = Globals.grid.flipped + sprite.flip_h = Globals.grid.flipped + + if type == Chess.PAWN: + popup = PopupPanel.new() + popup.popup_exclusive = true + popup.add_stylebox_override("panel", StyleBoxEmpty.new()) + previews = VBoxContainer.new() + 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.to_lower(), p.to_upper()] + newsprite.texture_normal = load(img_path) + newsprite.name = p + newsprite.connect("pressed", self, "_pressed", [p]) + previews.add_child(newsprite) + + +func _pressed(p: String) -> void: + popup.hide() + $"../../Darken".hide() + promote_to = p + emit_signal("promotion_decided") + + +func open_promotion_previews(): + popup.set_as_minsize() + var rect := popup.get_global_rect() + rect.position = rect_global_position + popup.popup(rect) + $"../../Darken".show() + + +func load_texture(path := "res://assets/pieces/%s/%s%s.png" % [Globals.piece_set, color, type.to_upper()]) -> void: + sprite.texture = load(path) + + +func set_zindex(zindex: int, obj: CanvasItem = self) -> void: # used by the animation player + VisualServer.canvas_item_set_z_index(obj.get_canvas_item(), zindex) + + +# returns self for function chaining +func move(to: String) -> Piece: + Globals.grid.set_piece(position, null) + Globals.grid.set_piece(to, self) + var go_to = Chess.algebraic2vec(to) + tween.interpolate_property( + self, "rect_position", rect_position, go_to * Globals.grid.piece_size, 0.3, Tween.TRANS_BACK + ) + var signresult := int(sign(Chess.algebraic2vec(position).x - go_to.x)) + if signresult == 1: + rotate.play("Right") + elif signresult == -1: + rotate.play("Left") + anim.play("Move") + tween.start() + position = to + return self + + +func took() -> void: + Globals.grid.set_piece(position, null) + frame.hide() + anim.play("Took") diff --git a/Piece.tscn b/piece/Piece.tscn index b643380..1353492 100644 --- a/Piece.tscn +++ b/piece/Piece.tscn @@ -1,10 +1,8 @@ -[gd_scene load_steps=9 format=2] +[gd_scene load_steps=8 format=2] -[ext_resource path="res://pieces/Piece.gd" type="Script" id=1] -[ext_resource path="res://assets/pieces/california/wP.png" type="Texture" id=2] -[ext_resource path="res://frame.png" type="Texture" id=3] - -[sub_resource type="StyleBoxEmpty" id=10] +[ext_resource path="res://assets/pieces/california/wP.png" type="Texture" id=1] +[ext_resource path="res://frame.png" type="Texture" id=2] +[ext_resource path="res://piece/Piece.gd" type="Script" id=3] [sub_resource type="Animation" id=1] resource_name = "Move" @@ -105,26 +103,13 @@ tracks/0/keys = { "values": [ 0.0, 20.0, 0.0 ] } -[node name="Piece" type="Control"] +[node name="Piece" type="Control" groups=["piece"]] margin_right = 80.0 margin_bottom = 80.0 rect_min_size = Vector2( 80, 80 ) rect_pivot_offset = Vector2( 40, 40 ) mouse_filter = 2 -script = ExtResource( 1 ) - -[node name="Popup" type="PopupPanel" parent="."] -margin_right = 40.0 -margin_bottom = 40.0 -mouse_filter = 2 -custom_styles/panel = SubResource( 10 ) -popup_exclusive = true - -[node name="Previews" type="VBoxContainer" parent="Popup"] -anchor_right = 1.0 -anchor_bottom = 1.0 -mouse_filter = 2 -custom_constants/separation = 0 +script = ExtResource( 3 ) [node name="ColorRect" type="ColorRect" parent="."] visible = false @@ -137,7 +122,7 @@ color = Color( 0.0784314, 0.333333, 0.117647, 0.498039 ) anchor_right = 1.0 anchor_bottom = 1.0 mouse_filter = 2 -texture = ExtResource( 2 ) +texture = ExtResource( 1 ) expand = true [node name="Frame" type="TextureRect" parent="Sprite"] @@ -145,11 +130,9 @@ visible = false anchor_right = 1.0 anchor_bottom = 1.0 mouse_filter = 2 -texture = ExtResource( 3 ) +texture = ExtResource( 2 ) expand = true -[node name="Tween" type="Tween" parent="."] - [node name="AnimationPlayer" type="AnimationPlayer" parent="."] anims/Move = SubResource( 1 ) anims/Took = SubResource( 2 ) diff --git a/pieces/B.gd b/pieces/B.gd deleted file mode 100644 index 18029fb..0000000 --- a/pieces/B.gd +++ /dev/null @@ -1,7 +0,0 @@ -extends Piece -class_name Bishop, "res://assets/pieces/california/wB.png" - - -func get_moves(no_enemys := false, check_spots_check := true) -> PoolVector2Array: - var dirs = PoolVector2Array(Array(all_dirs()).slice(4, 8)) - return traverse(dirs, no_enemys, check_spots_check) diff --git a/pieces/K.gd b/pieces/K.gd deleted file mode 100644 index ea253cc..0000000 --- a/pieces/K.gd +++ /dev/null @@ -1,89 +0,0 @@ -extends Piece -class_name King, "res://assets/pieces/california/wK.png" - -var castle_check := true -var can_castle := [] - -enum { NONE, QUEEN_SIDE, KING_SIDE } # keep up to date with move.movekind.castletypes - - -func _ready() -> void: - if white: - Globals.white_king = self - else: - Globals.black_king = self - Events.connect("just_before_turn_over", self, "just_before_over") - - -func get_moves(no_enemys := false, check_spots_check := true) -> PoolVector2Array: - var moves: PoolVector2Array = [] - for i in all_dirs(): - var spot := pos_around(i) - if is_on_board(spot): - if no_enemys and at_pos(spot): - continue - if check_spots_check and checkcheck(spot): - continue - moves.append(spot) - if castle_check and !Globals.in_check: # make sure this is only called when clicking - moves.append_array(castleing()) - return moves - - -func just_before_over() -> void: # assign metadata for threefold repetition draw check - castleing() - - -func castleing(justcheckrooks := false) -> Array: - if has_moved: - return [] - var moves := [] - var rooks := [pos_around(Vector2.RIGHT * 3), pos_around(Vector2.LEFT * 4)] - var labels = ["Q", "K"] - var rook_motion := [pos_around(Vector2.RIGHT), pos_around(Vector2.LEFT)] - var king_moveto_spots := [Vector2.RIGHT, Vector2.LEFT] # O-O and O-O-O respectivel - for i in range(len(rooks)): - if !is_on_board(rooks[i]): - continue - var rook: Piece = at_pos(rooks[i]) - if !rook is Rook: - continue - if rook.has_moved: - continue - if justcheckrooks: - moves.append(labels[i]) - continue - var direction: Vector2 = king_moveto_spots[i] - var posx2 := pos_around(direction * 2) - var pos := pos_around(direction) - if at_pos(posx2) or at_pos(pos) or checkcheck(posx2) or checkcheck(pos): - continue - if i == 1: # 3x check for O-O-O - var posx3 := pos_around(direction * 3) - if at_pos(posx3) or checkcheck(posx3): - continue - can_castle.append([posx2, rook, rook_motion[i], QUEEN_SIDE if i == 1 else KING_SIDE]) - moves.append(posx2) - if justcheckrooks: - moves.sort() - return moves - - -# basically a wrapper for move to -func castle(position: Vector2, instant := false) -> void: - can_castle.clear() - moveto(position, instant) - - -func can_move() -> bool: # checks if you can legally move - castle_check = false - var can := .can_move() - castle_check = true - return can - - -func get_attacks(check_spots_check := true) -> PoolVector2Array: - castle_check = false - var final := .get_attacks(check_spots_check) - castle_check = true - return final diff --git a/pieces/N.gd b/pieces/N.gd deleted file mode 100644 index 16dc101..0000000 --- a/pieces/N.gd +++ /dev/null @@ -1,22 +0,0 @@ -extends Piece -class_name Knight, "res://assets/pieces/california/wN.png" - - -func get_moves(no_enemys := false, check_spots_check := true) -> PoolVector2Array: - var moves: PoolVector2Array = [ - pos_around(Vector2(-2, -1)), - pos_around(Vector2(-2, 1)), - pos_around(Vector2(2, -1)), - pos_around(Vector2(2, 1)), - pos_around(Vector2(-1, -2)), - pos_around(Vector2(1, -2)), - pos_around(Vector2(-1, 2)), - pos_around(Vector2(1, 2)) - ] - var final: PoolVector2Array = [] - for i in moves: - if is_on_board(i): - if no_enemys and at_pos(i) or (check_spots_check and checkcheck(i)): - continue - final.append(i) - return final diff --git a/pieces/P.gd b/pieces/P.gd deleted file mode 100644 index 55c62d9..0000000 --- a/pieces/P.gd +++ /dev/null @@ -1,153 +0,0 @@ -extends Piece -class_name Pawn, "res://assets/pieces/california/wP.png" - -const promotables := "QNRB" - -var just_double_stepped := false -var just_set := false -var enpassant: Array = [] - -var promoteposition := Vector2() -var promotetake := false - -onready var whiteint := 1 if white else -1 -onready var sprites := [] -onready var darken: ColorRect = $"../../Darken" -onready var previews = $Popup/Previews -onready var popup: Popup = $Popup - - -func _ready() -> void: - Globals.pawns.append(self) - Events.connect("turn_over", self, "_on_turn_over") - for i in range(4): # add 4 sprites - var newsprite: TextureButton = load("res://ui/PromotionPreview.tscn").instance() - newsprite.texture_normal = load( - "res://assets/pieces/%s/%s%s.png" % [Globals.piece_set, team.to_lower(), promotables[i]] - ) - newsprite.name = promotables[i] - newsprite.connect("pressed", self, "_pressed", [newsprite.name]) - previews.add_child(newsprite) - sprites.append(newsprite) - - -func open_previews() -> void: - var rect := popup.get_global_rect() - rect.position = rect_global_position - popup.popup(rect) - - -func _exit_tree() -> void: - Globals.pawns.erase(self) - - -func moveto(position: Vector2, instant := false) -> void: - # check if 2 step - if !just_double_stepped and !has_moved: - just_double_stepped = true - just_set = true - .moveto(position, instant) - - -func get_moves(_var := false, check_spots_check := true) -> PoolVector2Array: - var points: PoolVector2Array = [Vector2.UP, Vector2.UP * 2] - var moves: PoolVector2Array = [] - for i in range(len(points)): - var point := points[i] - point *= whiteint - point = pos_around(point) - if at_pos(point) == null: - if ( - (i == 1 and (has_moved or at_pos(pos_around(points[0] * whiteint)) != null)) - or (check_spots_check and checkcheck(point)) - or !is_on_board(point) - ): - continue - moves.append(point) - moves.append_array(en_passant()) - return moves - - -static func can_promote(position: Vector2) -> bool: - return position.y >= 7 or position.y <= 0 - - -func passant(position: Vector2, instant := false) -> void: - var to_take = position + Vector2(0, whiteint) - at_pos(to_take).took(instant) - enpassant.resize(0) - moveto(position, instant) - - -func valid_to_passant_take(piece) -> bool: - return !piece or !Utils.is_pawn(piece) or piece.white != white or !piece.just_double_stepped - - -func get_attacks(check_spots_check := true) -> PoolVector2Array: - var points := [Vector2.UP + Vector2.RIGHT, Vector2.UP + Vector2.LEFT] - var moves: PoolVector2Array = [] - for i in range(len(points)): - var point: Vector2 = points[i] - point *= whiteint - point = pos_around(point) - if at_pos(point) == null or at_pos(point).white == white or (check_spots_check and checkcheck(point)): - continue - moves.append(point) - return moves - - -func en_passant(turncheck := true, check_spots_check := true) -> Array: # in passing - if turncheck and white != Globals.turn: - return [] - var passants := [pos_around(Vector2.LEFT), pos_around(Vector2.RIGHT)] - var moves := [] - for i in passants: - var spot := at_pos(i) - if ( - !spot # spot doesnt exist - or spot.white == white # spot is my team - or !Utils.is_pawn(spot) #spot isnt a pawn - or !spot.just_double_stepped # spot didnt just double step - or (check_spots_check and checkcheck(i)) - ): # moving there would put me in check - continue - var position: Vector2 = i + (Vector2.UP * whiteint) - if !at_pos(position): - moves.append(position) - enpassant.append([position, spot]) - return moves - - -func promote(position: Vector2, type: String) -> void: - if type == "take": - at_pos(position).hide() # only hide the visuals - move(position) # only move the visuals - promoteposition = position # save the position - darken.show() # open fx - yield(tween, "tween_completed") # wait till were done moving to the new position - open_previews() # open the previews - - -func promote_to(promote_to: int, is_capture: bool, position: Vector2, instant := false): - if is_capture and at_pos(position): - at_pos(position).took(instant) - clear_clicked() - Globals.grid.make_piece(position, promote_to, white) - took() - - -func _pressed(promote_to: String) -> void: - previews.hide() - darken.hide() - var is_cap = at_pos(promoteposition) != null - var mov = Move.new(SanParser.PAWN, [real_position, promoteposition], is_cap) - mov.promotion = SanParse.from_str(promote_to) - PacketHandler.send_mov(mov) - - -func _on_turn_over() -> void: - if just_set: - just_set = false - return - if just_double_stepped: - just_double_stepped = false diff --git a/pieces/Piece.gd b/pieces/Piece.gd deleted file mode 100644 index 5fa6f2c..0000000 --- a/pieces/Piece.gd +++ /dev/null @@ -1,222 +0,0 @@ -extends Control -class_name Piece, "res://assets/pieces/california/wP.png" - -var real_position := Vector2.ZERO -var white := true -var shortname := "" -var mininame := "♙" -var has_moved := false -var frameon := false -var team := "w" - -onready var sprite := $Sprite -onready var tween := $Tween -onready var anim := $AnimationPlayer -onready var rotate := $RotatePlayer -onready var colorrect := $ColorRect -onready var frame := $Sprite/Frame - - -func _ready() -> void: - team = "w" if white else "b" - var tmp: Array = Utils.get_node_name(self) - mininame = tmp[0] - shortname = tmp[1] - rect_min_size = Globals.grid.piece_size - rect_size = rect_min_size - rect_pivot_offset = Globals.grid.piece_size / 2 - frame.modulate = Globals.grid.overlay_color - colorrect.color = Globals.grid.overlay_color - load_texture() - - # for undos - sprite.flip_v = Globals.grid.flipped - sprite.flip_h = Globals.grid.flipped - - -func set_zindex(zindex: int, obj: CanvasItem = self): # used by the animation player - VisualServer.canvas_item_set_z_index(obj.get_canvas_item(), zindex) - - -func load_texture(path := "%s%s%s.png" % [Globals.grid.ASSETS_PATH, team, shortname.to_upper()]) -> void: - sprite.texture = load(path) - - -func clicked() -> void: - colorrect.show() - set_circle(get_moves()) - set_circle(get_attacks(), "take") - - -func clear_clicked() -> void: - colorrect.hide() - Globals.grid.clear_fx() - - -func move(newpos: Vector2) -> void: # dont use directly; use moveto - tween.interpolate_property( - self, "rect_position", rect_position, newpos * Globals.grid.piece_size, 0.3, Tween.TRANS_BACK - ) - var signresult := int(sign(real_position.x - newpos.x)) - if signresult == 1: - rotate.play("Right") - elif signresult == -1: - rotate.play("Left") - anim.play("Move") - tween.start() - - -func moveto(pos: Vector2, instant := false) -> void: - Globals.grid.matrix[real_position.y][real_position.x] = null - Globals.grid.matrix[pos.y][pos.x] = self - if instant: - real_position = pos - else: - move(pos) - real_position = pos - SoundFx.play("Move") - has_moved = true - - -func update_visual_position(): - rect_position = real_position * Globals.grid.piece_size - - -func pos_around(around_vector: Vector2) -> Vector2: - return real_position + around_vector - - -static func all_dirs() -> PoolVector2Array: - return PoolVector2Array( - [ - Vector2.UP, - Vector2.DOWN, - Vector2.LEFT, - Vector2.RIGHT, - Vector2(1, 1), - Vector2(1, -1), - Vector2(-1, 1), - Vector2(-1, -1) - ] - ) - - -func traverse(arr: PoolVector2Array = [], no_enemys := false, check_spots_check := true) -> PoolVector2Array: - var circle_array: PoolVector2Array = [] - for i in arr: - var pos := real_position - while true: - pos += i - if !is_on_board(pos): - break - if at_pos(pos) != null: - if at_pos(pos).white == white: # fren - break - # certaintly a enemy - if no_enemys: # do we want enemys? - break - circle_array.append(pos) - break - if check_spots_check and checkcheck(pos): - continue - circle_array.append(pos) - return circle_array - - -static func at_pos(vector: Vector2) -> Piece: - if is_on_board(vector): - return Globals.grid.matrix[vector.y][vector.x] - return null - - -func can_move() -> bool: # checks if you can legally move - return not get_moves().empty() or not get_attacks().empty() - - -func get_moves(_no_enemys := false, _check_spots_check := true) -> PoolVector2Array: # plz override - return all_dirs() - - -func get_attacks(check_spots_check := true) -> PoolVector2Array: # @Override - var moves := get_moves(false, check_spots_check) # assumes the attacks are same as moves - var final: PoolVector2Array = [] - for i in moves: - if at_pos(i) != null: - if at_pos(i).white != white: # attack ze enemie - if check_spots_check and checkcheck(i): - continue - final.append(i) - return final - - -func can_attack_piece(piece: Piece) -> bool: ##i cant use pos in get_attacks for some bizarre reasons - for pos in get_attacks(false): - if at_pos(pos) == piece: - return true - return false - - -func can_touch(pos: Vector2, attack := true, move := true) -> bool: - if attack and move: - return pos in get_attacks() or pos in get_moves() - elif attack: - return pos in get_attacks() - elif move: - return pos in get_moves() - else: - return false - - -static func create_move_circles(pos: Vector2) -> void: - Globals.grid.get_background_element(pos).set_circle(true) # make the move circle - - -static func create_take_circles(spot: Piece) -> void: # create take circles - spot.set_frame() # turn on the little take frame on the piece, to show its takeable - - -static func set_circle(positions: Array, type := "move") -> void: - for pos in positions: - var spot := at_pos(pos) # get the piece at the position - if type == "move": - create_move_circles(pos) # create the move circle - elif type == "take": - create_take_circles(spot) # if the king is in check, return true - - -func checkcheck(pos) -> bool: # moves to position, then checks if your king is in check - # TODO: figure out why this function isnt working with can_move() - if Globals.turn != white: - Log.err("@checkcheck: %s != %s" % [Globals.turn, white]) - var mat: Array = Globals.grid.matrix.duplicate(true) # make a copy of the matrix - Globals.grid.matrix[real_position.y][real_position.x] = null # remove the piece from the matrix - Globals.grid.matrix[pos.y][pos.x] = self # move the piece to the new position - if Globals.grid.check_in_check(): # if you are still in check - Globals.grid.matrix = mat # revert changes on the matrix - return true - Globals.grid.matrix = mat - return false - - -static func is_on_board(vector: Vector2) -> bool: # limit the vector to the board - if vector.y < 0 or vector.y > 7 or vector.x < 0 or vector.x > 7: - return false - return true - - -func take(piece: Piece, instant := false) -> void: - clear_clicked() - piece.took(instant) - moveto(piece.real_position, instant) - - -func took(instant := false) -> void: # called when piece is taken - Globals.grid.matrix[real_position.y][real_position.x] = null - if !instant: - SoundFx.play("Capture") - anim.play("Took") - - -func set_frame(value := true) -> void: - frameon = value - frame.visible = value diff --git a/pieces/Q.gd b/pieces/Q.gd deleted file mode 100644 index 6143b16..0000000 --- a/pieces/Q.gd +++ /dev/null @@ -1,6 +0,0 @@ -extends Piece -class_name Queen, "res://assets/pieces/california/wQ.png" - - -func get_moves(no_enemys := false, check_spots_check := true) -> PoolVector2Array: - return traverse(all_dirs(), no_enemys, check_spots_check) diff --git a/pieces/R.gd b/pieces/R.gd deleted file mode 100644 index 40150d8..0000000 --- a/pieces/R.gd +++ /dev/null @@ -1,6 +0,0 @@ -extends Piece -class_name Rook, "res://assets/pieces/california/wR.png" - - -func get_moves(no_enemys := false, check_spots_check := true) -> PoolVector2Array: - return traverse([Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT], no_enemys, check_spots_check) diff --git a/project.godot b/project.godot index aeb90af..124edab 100644 --- a/project.godot +++ b/project.godot @@ -9,16 +9,16 @@ config_version=4 _global_script_classes=[ { +"base": "ColorRect", +"class": "BackgroundSquare", +"language": "GDScript", +"path": "res://Square.gd" +}, { "base": "TextureButton", "class": "BarTextureButton", "language": "GDScript", "path": "res://ui/barbutton/BarTextureButton.gd" }, { -"base": "Piece", -"class": "Bishop", -"language": "GDScript", -"path": "res://pieces/B.gd" -}, { "base": "KeyUtils", "class": "CapsLock", "language": "GDScript", @@ -34,6 +34,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://ui/checkboxbutton/CheckBoxButton.gd" }, { +"base": "Node", +"class": "Chess", +"language": "GDScript", +"path": "res://board/chess.gd" +}, { "base": "Control", "class": "ColorPickerBetter", "language": "GDScript", @@ -74,11 +79,6 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://FEN/Fen.gd" }, { -"base": "LineEdit", -"class": "FENLabel", -"language": "GDScript", -"path": "res://ui/menus/sidebarright/FENlabel.gd" -}, { "base": "Button", "class": "FlipButton", "language": "GDScript", @@ -119,16 +119,6 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://ui/virtual_keyboard/KeyUtils.gd" }, { -"base": "Piece", -"class": "King", -"language": "GDScript", -"path": "res://pieces/K.gd" -}, { -"base": "Piece", -"class": "Knight", -"language": "GDScript", -"path": "res://pieces/N.gd" -}, { "base": "Control", "class": "Lobby", "language": "GDScript", @@ -139,11 +129,6 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://ui/chat/MessageList.gd" }, { -"base": "Resource", -"class": "Move", -"language": "GDScript", -"path": "res://SanParse/Move.gd" -}, { "base": "Node", "class": "Network", "language": "GDScript", @@ -159,25 +144,20 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://PGN/PGN.gd" }, { -"base": "Piece", -"class": "Pawn", -"language": "GDScript", -"path": "res://pieces/P.gd" -}, { "base": "Control", "class": "Piece", "language": "GDScript", -"path": "res://pieces/Piece.gd" +"path": "res://piece/Piece.gd" }, { "base": "GridContainer", "class": "Preview", "language": "GDScript", "path": "res://ui/menus/settings/Preview.gd" }, { -"base": "Piece", -"class": "Queen", +"base": "TextureButton", +"class": "PromotionPreview", "language": "GDScript", -"path": "res://pieces/Q.gd" +"path": "res://ui/PromotionPreview.gd" }, { "base": "ConfirmButton", "class": "ResignButton", @@ -189,16 +169,6 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://ui/menus/account/Restrict.gd" }, { -"base": "Piece", -"class": "Rook", -"language": "GDScript", -"path": "res://pieces/R.gd" -}, { -"base": "Node", -"class": "SanParser", -"language": "GDScript", -"path": "res://SanParse/SanParse.gd" -}, { "base": "Node", "class": "SaveLoader", "language": "GDScript", @@ -240,11 +210,12 @@ _global_script_classes=[ { "path": "res://ui/virtual_keyboard/VirtualKeyboard.gd" } ] _global_script_class_icons={ +"BackgroundSquare": "", "BarTextureButton": "", -"Bishop": "res://assets/pieces/california/wB.png", "CapsLock": "", "Chat": "", "CheckBoxButton": "", +"Chess": "", "ColorPickerBetter": "", "ColorPickerButtonBetter": "", "ColorSelect": "", @@ -253,7 +224,6 @@ _global_script_class_icons={ "DrawButton": "", "ExpandableTextEdit": "", "FEN": "", -"FENLabel": "", "FlipButton": "", "Grid": "", "GridMenu": "", @@ -262,22 +232,16 @@ _global_script_class_icons={ "InfoLabel": "", "Key": "", "KeyUtils": "", -"King": "res://assets/pieces/california/wK.png", -"Knight": "res://assets/pieces/california/wN.png", "Lobby": "", "MessageList": "", -"Move": "", "Network": "", "OldColorView": "", "PGN": "", -"Pawn": "res://assets/pieces/california/wP.png", -"Piece": "res://assets/pieces/california/wP.png", +"Piece": "", "Preview": "", -"Queen": "res://assets/pieces/california/wQ.png", +"PromotionPreview": "", "ResignButton": "", "Restrict": "", -"Rook": "res://assets/pieces/california/wR.png", -"SanParser": "", "SaveLoader": "", "SliderButton": "", "SpecialKey": "", @@ -310,7 +274,6 @@ SaveLoad="*res://saveload.gd" ColorBack="*res://ui/background/ColorfullBackground.tscn" PacketHandler="*res://networking/PacketHandler.gd" Debug="*res://Debug.gd" -SanParse="*res://SanParse/SanParse.gd" Pgn="*res://PGN/PGN.gd" Log="*res://Log.gd" Fen="*res://FEN/Fen.gd" diff --git a/sounds/Defeat.ogg b/sounds/Defeat.ogg deleted file mode 100644 index e5ffd86..0000000 --- a/sounds/Defeat.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0f8dec47f2698f62a769f84d79d4903c1475224cd9e40c53dc2fe5d2c0e7d5c -size 8883 diff --git a/sounds/Defeat.ogg.import b/sounds/Defeat.ogg.import deleted file mode 100644 index 63843f7..0000000 --- a/sounds/Defeat.ogg.import +++ /dev/null @@ -1,15 +0,0 @@ -[remap] - -importer="ogg_vorbis" -type="AudioStreamOGGVorbis" -path="res://.import/Defeat.ogg-a3453b9d2f260a588582234d5fa9ba76.oggstr" - -[deps] - -source_file="res://sounds/Defeat.ogg" -dest_files=[ "res://.import/Defeat.ogg-a3453b9d2f260a588582234d5fa9ba76.oggstr" ] - -[params] - -loop=false -loop_offset=0 diff --git a/sounds/Draw.ogg b/sounds/Draw.ogg deleted file mode 100644 index e5ffd86..0000000 --- a/sounds/Draw.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0f8dec47f2698f62a769f84d79d4903c1475224cd9e40c53dc2fe5d2c0e7d5c -size 8883 diff --git a/sounds/Draw.ogg.import b/sounds/Draw.ogg.import deleted file mode 100644 index 4aa00da..0000000 --- a/sounds/Draw.ogg.import +++ /dev/null @@ -1,15 +0,0 @@ -[remap] - -importer="ogg_vorbis" -type="AudioStreamOGGVorbis" -path="res://.import/Draw.ogg-9c657519a42c1bcc6f5fc6b90af52d95.oggstr" - -[deps] - -source_file="res://sounds/Draw.ogg" -dest_files=[ "res://.import/Draw.ogg-9c657519a42c1bcc6f5fc6b90af52d95.oggstr" ] - -[params] - -loop=false -loop_offset=0 diff --git a/sounds/SoundFX.gd b/sounds/SoundFX.gd index 3e6862c..1a27d71 100644 --- a/sounds/SoundFX.gd +++ b/sounds/SoundFX.gd @@ -3,12 +3,10 @@ extends Node const soundpath := "res://sounds/" var sounds := { + "Capture": load(soundpath + "Capture.ogg"), "Check": load(soundpath + "Check.ogg"), "Error": load(soundpath + "Error.ogg"), "Victory": load(soundpath + "Victory.ogg"), - "Defeat": load(soundpath + "Defeat.ogg"), - "Capture": load(soundpath + "Capture.ogg"), - "Draw": load(soundpath + "Draw.ogg"), "Move": load(soundpath + "Move.ogg"), } diff --git a/test.gd b/test.gd deleted file mode 100644 index 735500b..0000000 --- a/test.gd +++ /dev/null @@ -1,115 +0,0 @@ -extends Node - - -class TestSan: - extends SanParser - - func assert_castle(string: String, s): - var m = parse(string) - assert(m.move_kind.type == Move.MoveKind.CASTLE) - assert(m.move_kind.data == s) - assert(m.piece == KING) - assert(m.promotion == -1) - assert(m.check_type == Move.CHECKTYPES.NONE) - assert(m.is_capture == false) - - func assert_move(mv: String, start: Vector2, dest: Vector2, piece: int) -> void: - assert_all(parse(mv), PoolVector2Array([start, dest]), piece, false) - - func assert_capture(mv: String, start: Vector2, dest: Vector2, piece: int) -> void: - assert_all(parse(mv), PoolVector2Array([start, dest]), piece, true) - - func assert_all(mv: Move, vectors: PoolVector2Array, piece: int, capture: bool, promote = -1) -> void: - assert(mv.move_kind.type == Move.MoveKind.NORMAL) - assert([mv.move_kind.data == vectors, mv.piece == piece, mv.is_capture == capture].min()) - if promote != -1: - assert(mv.promotion == promote) - - func test_algebraic_conversion(): - assert(Utils.from_algebraic(Utils.to_algebraic(Vector2.ZERO)) == Vector2.ZERO) - assert(Utils.from_algebraic(Utils.to_algebraic(Vector2.ONE)) == Vector2.ONE) - assert(pos("a", "9") == Utils.from_algebraic("a9")) - assert(Utils.col_pos("h") == 7) - assert(Utils.row_pos("1") == 7) - - func test_castle_short(): - assert_castle("O-O", Move.MoveKind.CASTLETYPES.KING_SIDE) - - func test_castle_long(): - assert_castle("O-O-O", Move.MoveKind.CASTLETYPES.QUEEN_SIDE) - - func test_pawn(): - assert_move("e4", UNKNOWN_POS, Vector2(4, 4), PAWN) - - func test_pawn_long(): - assert_move("e2e4", Vector2(4, 6), Vector2(4, 4), PAWN) - - func test_piece(): - assert_move("Qe4", UNKNOWN_POS, Vector2(4, 4), QUEEN) - - func test_piece_file(): - assert_move("Qbe4", Vector2(1, -1), Vector2(4, 4), QUEEN) - - func test_piece_rank(): - assert_move("Q1e4", Vector2(-1, 7), Vector2(4, 4), QUEEN) - - func test_piece_long(): - assert_move("Qb1e4", Vector2(1, 7), Vector2(4, 4), QUEEN) - - func test_pawn_capture(): - assert_capture("exd4", Vector2(4, -1), Vector2(3, 4), PAWN) - - func test_pawn_capture_promotion(): - assert_all(parse("exd8=Q"), PoolVector2Array([Vector2(4, -1), Vector2(3, 0)]), PAWN, true, QUEEN) - - func test_pawn_capture_long(): - assert_capture("e3xd4", Vector2(4, 5), Vector2(3, 4), PAWN) - - func test_row_piece_capture(): - assert_capture("R1xh3", Vector2(-1, 7), Vector2(7, 5), ROOK) - - func test_piece_capture(): - assert_capture("Nxe4", Vector2(-1, -1), Vector2(4, 4), KNIGHT) - - func test_piece_capture_file(): - assert_capture("Rexh3", Vector2(4, -1), Vector2(7, 5), ROOK) - - func test_piece_capture_long(): - assert_capture("Re3xh3", Vector2(4, 5), Vector2(7, 5), ROOK) - - func test_pawn_promotion(): - assert_all(parse("d8=Q"), PoolVector2Array([UNKNOWN_POS, Vector2(3, 0)]), PAWN, QUEEN) - - func test_compile(): - var s = Move.new(PAWN, [Vector2(4, -1), Vector2(3, 0)]) - s.promotion = QUEEN - s.check_type = Move.CHECKTYPES.CHECK - s.is_capture = true - var result = s.compile() - assert(result == "exd8=Q+") - assert((SanParse.parse("e4").compile()) == "e4") - - func _init(): - test_algebraic_conversion() - test_castle_short() - test_castle_long() - test_pawn() - test_pawn_long() - test_piece() - test_piece_file() - test_piece_rank() - test_piece_long() - test_pawn_capture() - test_row_piece_capture() - test_pawn_capture_promotion() - test_pawn_capture_long() - test_piece_capture() - test_piece_capture_file() - test_piece_capture_long() - test_pawn_promotion() - test_compile() - - -func _ready(): - if Debug.debug: - TestSan.new() diff --git a/ui/PromotionPreview.gd b/ui/PromotionPreview.gd index 726dd63..68ddf3c 100644 --- a/ui/PromotionPreview.gd +++ b/ui/PromotionPreview.gd @@ -1,4 +1,5 @@ extends TextureButton +class_name PromotionPreview var focused = false setget set_focused @@ -9,7 +10,11 @@ func set_focused(is_focused: bool): func _ready(): - if Globals.grid: - rect_pivot_offset = Globals.grid.piece_size / 2 - rect_min_size = Globals.grid.piece_size + connect("mouse_entered", self, "set_focused", [true]) + connect("mouse_exited", self, "set_focused", [false]) + stretch_mode = STRETCH_KEEP_ASPECT_CENTERED + mouse_default_cursor_shape = CURSOR_POINTING_HAND + expand = true + rect_pivot_offset = Globals.grid.piece_size / 2 + rect_min_size = Globals.grid.piece_size set_focused(false) diff --git a/ui/PromotionPreview.tscn b/ui/PromotionPreview.tscn deleted file mode 100644 index 9bf892e..0000000 --- a/ui/PromotionPreview.tscn +++ /dev/null @@ -1,24 +0,0 @@ -[gd_scene load_steps=3 format=2] - -[ext_resource path="res://ui/PromotionPreview.gd" type="Script" id=1] -[ext_resource path="res://assets/pieces/california/wP.png" type="Texture" id=2] - -[node name="PromotionPreview" type="TextureButton"] -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -margin_left = -711.0 -margin_top = -400.0 -margin_right = -631.0 -margin_bottom = -320.0 -rect_min_size = Vector2( 80, 80 ) -rect_pivot_offset = Vector2( 40, 40 ) -mouse_filter = 1 -texture_normal = ExtResource( 2 ) -expand = true -stretch_mode = 5 -script = ExtResource( 1 ) - -[connection signal="mouse_entered" from="." to="." method="set_focused" binds= [ true ]] -[connection signal="mouse_exited" from="." to="." method="set_focused" binds= [ false ]] diff --git a/ui/chat/Chat.gd b/ui/chat/Chat.gd index 5cc2d97..5543783 100644 --- a/ui/chat/Chat.gd +++ b/ui/chat/Chat.gd @@ -6,18 +6,21 @@ onready var kb = $v/Keyboard onready var dsk_input: TextEditor = $v/DesktopInput var regexes := [ - [compile("_([^_]+)_"), "[i]$1[/i]"], - [compile("\\*\\*([^\\*\\*]+)\\*\\*"), "[b]$1[/b]"], - [compile("\\*([^\\*]+)\\*"), "[i]$1[/i]"], - [compile("```([^`]+)```"), "[code]$1[/code]"], - [compile("`([^`]+)`"), "[code]$1[/code]"], - [compile("~~([^~]+)~~"), "[s]$1[/s]"], - [compile("#([^#]+)#"), "[rainbow freq=.3 sat=.7]$1[/rainbow]"], - [compile("%([^%]+)%"), "[shake rate=20 level=25]$1[/shake]"], - [compile("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"), "[url=$2]$1[/url]"], - [compile("([-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*))"), "[url]$1[/url]"], + [Utils.compile("_([^_]+)_"), "[i]$1[/i]"], + [Utils.compile("\\*\\*([^\\*\\*]+)\\*\\*"), "[b]$1[/b]"], + [Utils.compile("\\*([^\\*]+)\\*"), "[i]$1[/i]"], + [Utils.compile("```([^`]+)```"), "[code]$1[/code]"], + [Utils.compile("`([^`]+)`"), "[code]$1[/code]"], + [Utils.compile("~~([^~]+)~~"), "[s]$1[/s]"], + [Utils.compile("#([^#]+)#"), "[rainbow freq=.3 sat=.7]$1[/rainbow]"], + [Utils.compile("%([^%]+)%"), "[shake rate=20 level=25]$1[/shake]"], + [Utils.compile("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"), "[url=$2]$1[/url]"], + [ + Utils.compile("([-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*))"), + "[url]$1[/url]" + ], ] -var emoji_replace_regex := compile(":[^:]{1,30}:") +var emoji_replace_regex: RegEx = Utils.compile(":[^:]{1,30}:") const piece_emoji_path = "res://assets/pieces/cburnett/" const emoji_path = "res://assets/emojis/" @@ -158,12 +161,6 @@ func _ready(): server("You can use markdown(sort of)!") # say hello again -static func compile(src: String) -> RegEx: - var regex := RegEx.new() - regex.compile(src) - return regex - - func add_label_with(data: Dictionary) -> void: var string := "[b]{who}[color=#f0e67e]:[/color][/b] {text}".format(data) list.add_label(string) @@ -176,7 +173,7 @@ func send(t: String) -> void: t = md2bb(emoji2bb(t)) var name_data = SaveLoad.get_data("id").name var name = name_data if name_data else "Anonymous" - name += "(%s)" % ("Spectator" if Globals.spectating else Globals.get_team()) + name += "(%s)" % ("Spectator" if Globals.spectating else Globals.team) if PacketHandler.connected: PacketHandler.relay_signal({"text": t, "who": name}, PacketHandler.RELAYHEADERS.chat) else: diff --git a/ui/menus/StartMenu.tscn b/ui/menus/StartMenu.tscn index 0c14f81..7c9e210 100644 --- a/ui/menus/StartMenu.tscn +++ b/ui/menus/StartMenu.tscn @@ -1,13 +1,12 @@ -[gd_scene load_steps=10 format=2] +[gd_scene load_steps=9 format=2] [ext_resource path="res://ui/menus/account/Account.tscn" type="PackedScene" id=1] [ext_resource path="res://ui/menus/settings/Settings.tscn" type="PackedScene" id=2] [ext_resource path="res://assets/ui/ubuntu/ubuntu-bold.ttf" type="DynamicFontData" id=3] [ext_resource path="res://ui/theme/main.theme" type="Theme" id=4] [ext_resource path="res://ui/menus/lobby/Lobby.tscn" type="PackedScene" id=5] -[ext_resource path="res://test.gd" type="Script" id=6] [ext_resource path="res://ui/menus/StartMenu.gd" type="Script" id=7] -[ext_resource path="res://PGN/test_pgns.tscn" type="PackedScene" id=8] +[ext_resource path="res://ui/menus/tests/test_runner.tscn" type="PackedScene" id=8] [sub_resource type="DynamicFont" id=1] size = 400 @@ -75,7 +74,4 @@ size_flags_vertical = 4 custom_fonts/font = SubResource( 1 ) text = "exit" -[node name="tests" type="Node2D" parent="."] -script = ExtResource( 6 ) - [connection signal="pressed" from="tabs/" to="." method="_on_quit_pressed"] diff --git a/ui/menus/lobby/GameConfig.gd b/ui/menus/lobby/GameConfig.gd index da0cd67..7fe77d3 100644 --- a/ui/menus/lobby/GameConfig.gd +++ b/ui/menus/lobby/GameConfig.gd @@ -28,7 +28,9 @@ func _on_Stop_pressed(): func reset(): - lobby.set_buttons(true) + moves = [] + white = true + $Advanced/H/Pgn.text_changed("") hide() diff --git a/ui/menus/lobby/GameConfig.tscn b/ui/menus/lobby/GameConfig.tscn index 1956929..66f123a 100644 --- a/ui/menus/lobby/GameConfig.tscn +++ b/ui/menus/lobby/GameConfig.tscn @@ -14,7 +14,6 @@ theme = ExtResource( 1 ) script = ExtResource( 5 ) [node name="General" type="VBoxContainer" parent="."] -visible = false anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 30.0 @@ -81,20 +80,32 @@ size_flags_horizontal = 4 text = "continue " [node name="Advanced" type="VBoxContainer" parent="."] +visible = false anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 30.0 -margin_top = 86.0 +margin_top = 56.0 margin_right = -30.0 margin_bottom = -30.0 -[node name="Pgn" type="LineEdit" parent="Advanced"] +[node name="H" type="HBoxContainer" parent="Advanced"] margin_right = 1362.0 margin_bottom = 102.0 + +[node name="Pgn" type="LineEdit" parent="Advanced/H"] +margin_right = 1297.0 +margin_bottom = 102.0 +size_flags_horizontal = 3 placeholder_text = "1. e4" script = ExtResource( 6 ) +[node name="Checkmark" type="Label" parent="Advanced/H"] +margin_left = 1312.0 +margin_top = 28.0 +margin_right = 1312.0 +margin_bottom = 74.0 + [connection signal="pressed" from="General/H2/Stop" to="." method="_on_Stop_pressed"] [connection signal="pressed" from="General/H2/Continue" to="." method="_on_Continue_pressed"] -[connection signal="pgn_selected" from="Advanced/Pgn" to="." method="_on_pgn_selected"] -[connection signal="text_entered" from="Advanced/Pgn" to="Advanced/Pgn" method="_text_entered"] +[connection signal="pgn_selected" from="Advanced/H/Pgn" to="." method="_on_pgn_selected"] +[connection signal="text_changed" from="Advanced/H/Pgn" to="Advanced/H/Pgn" method="text_changed"] diff --git a/ui/menus/lobby/Lobby.gd b/ui/menus/lobby/Lobby.gd index 3f6088c..4979f49 100644 --- a/ui/menus/lobby/Lobby.gd +++ b/ui/menus/lobby/Lobby.gd @@ -5,6 +5,7 @@ onready var address: LineEdit = find_node("Address") onready var buttons := find_node("buttons") onready var status_ok := find_node("StatusOK") onready var status_fail := find_node("StatusFail") +onready var hostbutton = find_node("HostButton") func toggle(onoff: bool) -> void: @@ -58,6 +59,7 @@ func _on_HostButton_pressed() -> void: if validate_text(): set_buttons(false) $Center/VBox/GameConfig.open(self) + hostbutton.disabled = true else: set_status("Invalid address", false) diff --git a/ui/menus/lobby/PGNEntry.gd b/ui/menus/lobby/PGNEntry.gd index 5f76aa2..7361911 100644 --- a/ui/menus/lobby/PGNEntry.gd +++ b/ui/menus/lobby/PGNEntry.gd @@ -1,19 +1,26 @@ extends LineEdit +onready var checkmark: Label = $"../Checkmark" + signal pgn_selected(m_array) -func _text_entered(new_text: String): +func text_changed(new_text: String) -> void: + if !new_text: + checkmark.text = "" + return var status = validate_pgn(new_text) if status: emit_signal("pgn_selected", status) + checkmark.text = "" else: - text = "invalid pgn" + checkmark.text = "" func validate_pgn(p: String): var parsed = Pgn.parse(p) - if parsed == null: - return false - else: - return parsed.moves # TODO: simulate the pgn and such nonsense + if parsed != null: + var c = Chess.new() + if c.load_pgn(text) == OK and !c.game_over(): + return parsed.moves + return false diff --git a/ui/menus/sidebarright/FENlabel.gd b/ui/menus/sidebarright/FENlabel.gd deleted file mode 100644 index 2dc0c91..0000000 --- a/ui/menus/sidebarright/FENlabel.gd +++ /dev/null @@ -1,10 +0,0 @@ -extends LineEdit -class_name FENLabel - - -func _ready() -> void: - Utils.connect("newfen", self, "on_new_fen") - - -func on_new_fen(fen: String) -> void: - text = fen diff --git a/ui/menus/sidebarright/SidebarRight.tscn b/ui/menus/sidebarright/SidebarRight.tscn index 37e35ea..b1850b6 100644 --- a/ui/menus/sidebarright/SidebarRight.tscn +++ b/ui/menus/sidebarright/SidebarRight.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=19 format=2] +[gd_scene load_steps=20 format=2] [ext_resource path="res://ui/menus/sidebarright/drawbutton.gd" type="Script" id=1] [ext_resource path="res://ui/menus/sidebarright/resignbutton.gd" type="Script" id=2] [ext_resource path="res://ui/Status.gd" type="Script" id=3] +[ext_resource path="res://ui/ubuntu.tres" type="DynamicFont" id=4] [ext_resource path="res://ui/barbutton/default_highlight.tres" type="StyleBox" id=5] [ext_resource path="res://ui/barbutton/default_pressed.tres" type="StyleBox" id=6] [ext_resource path="res://ui/barbutton/default.tres" type="StyleBox" id=7] @@ -111,6 +112,7 @@ hint_tooltip = "request a draw" focus_mode = 0 mouse_default_cursor_shape = 2 size_flags_horizontal = 3 +custom_fonts/font = ExtResource( 4 ) text = "½-½" script = ExtResource( 1 ) confirm_text = "Your opponent requests a draw" @@ -137,7 +139,7 @@ hint_tooltip = "request a undo" focus_mode = 0 mouse_default_cursor_shape = 2 size_flags_horizontal = 3 -text = "" +text = "社" script = ExtResource( 18 ) confirm_text = "Your opponent requests a undo" status = NodePath("../../../Status") diff --git a/ui/menus/sidebarright/Timer.gd b/ui/menus/sidebarright/Timer.gd deleted file mode 100644 index e97d8d7..0000000 --- a/ui/menus/sidebarright/Timer.gd +++ /dev/null @@ -1,22 +0,0 @@ -extends Node - -export(NodePath) var blacklabel -export(NodePath) var whitelabel -onready var labels = [get_node(blacklabel), get_node(whitelabel)] - -var turn_time := 0.0 - - -func _process(delta): - # int of false is 0 and true is 1 - turn_time += delta - labels[int(Globals.turn)].tick() - - -func _move_decided(): - prints("turn took", turn_time) - turn_time = 0.0 - - -func _ready(): - Globals.grid.connect("move_decided", self, "_move_decided") diff --git a/ui/menus/sidebarright/TimerLabels.gd b/ui/menus/sidebarright/TimerLabels.gd deleted file mode 100644 index 5cd1ed5..0000000 --- a/ui/menus/sidebarright/TimerLabels.gd +++ /dev/null @@ -1,47 +0,0 @@ -extends Label - -var time: float setget set_time -var stop := false - -const STARTTIME = 300 - -export(bool) var white := false - -onready var colorrect := $ColorRect - - -func set_time(newtime: float) -> bool: - if stop: - return false - time = newtime - if time <= 0.0: - Events.emit_signal("outoftime", white) - stop = true - text = "00:00.0" - return false - var use_millis := time <= 10 - text = Utils.format_seconds(time, use_millis) - return true - - -func tick(delta: float): - time -= delta - - -func _ready() -> void: - set_time(STARTTIME) - set_color() - colorrect.show_behind_parent = true - Events.connect("data_recieved", self, "set_color") - Events.connect("game_over", self, "_on_game_over") - - -func _on_game_over(_ar: String, _arg: bool) -> void: - stop = true - - -func set_color() -> void: - if time > 10: - colorrect.color = (Globals.grid.clockrunning_color if Globals.turn == white else Color.transparent) - else: - colorrect.color = (Globals.grid.clockrunninglow if Globals.turn == white else Globals.grid.clocklow) diff --git a/ui/menus/sidebarright/drawbutton.gd b/ui/menus/sidebarright/drawbutton.gd index 877a5ac..948506c 100644 --- a/ui/menus/sidebarright/drawbutton.gd +++ b/ui/menus/sidebarright/drawbutton.gd @@ -10,17 +10,17 @@ func _signal_recieved(what: Dictionary) -> void: set_disabled(false) if "question" in what: confirm() - Globals.chat.server(draw_request_message % Globals.str_bool(!Globals.team)) + Globals.chat.server(what.question) else: if what.accepted: - drawed() + draw() else: # declined signal recieved Globals.chat.server(draw_declined_message) -func drawed() -> GDScriptFunctionState: - return Globals.grid.drawed("mutual agreement") +func draw(): + Globals.grid.draw("mutual agreement") func _pressed() -> void: @@ -30,15 +30,16 @@ func _pressed() -> void: _confirmed(true) else: set_disabled(true) - PacketHandler.signal({"question": ""}, PacketHandler.SIGNALHEADERS.draw) - Globals.chat.server(draw_request_message % Globals.get_team()) + var msg = draw_request_message % Utils.expand_color(Globals.team) + PacketHandler.signal({question = msg}, PacketHandler.SIGNALHEADERS.draw) + Globals.chat.server(msg) func _confirmed(what: bool) -> void: # called from confirmbar.confirmed ._confirmed(what) PacketHandler.signal({"accepted": what}, PacketHandler.SIGNALHEADERS.draw) if what: - drawed() + draw() else: # no pressed Globals.chat.server(draw_declined_message) diff --git a/ui/menus/sidebarright/resignbutton.gd b/ui/menus/sidebarright/resignbutton.gd index 47b5946..e84f46e 100644 --- a/ui/menus/sidebarright/resignbutton.gd +++ b/ui/menus/sidebarright/resignbutton.gd @@ -18,5 +18,5 @@ func _pressed() -> void: func after_confirmed(): PacketHandler.signal({}, PacketHandler.SIGNALHEADERS.resign) - Globals.grid.win(!Globals.team, "resignation") + Globals.grid.win("w" if Globals.team == "b" else "b", "resignation") disabled = true diff --git a/ui/menus/sidebarright/sandisplay/Base.gd b/ui/menus/sidebarright/sandisplay/Base.gd index e236b49..2bf04bf 100644 --- a/ui/menus/sidebarright/sandisplay/Base.gd +++ b/ui/menus/sidebarright/sandisplay/Base.gd @@ -9,3 +9,10 @@ var moves_added = 0 func add_move(move: String) -> void: sans[moves_added].text = move moves_added += 1 + + +func pop_move(): + moves_added -= 1 + if moves_added == 0: + queue_free() + sans[moves_added].text = "" diff --git a/ui/menus/sidebarright/sandisplay/SanDisplay.gd b/ui/menus/sidebarright/sandisplay/SanDisplay.gd index 504fb55..502d321 100644 --- a/ui/menus/sidebarright/sandisplay/SanDisplay.gd +++ b/ui/menus/sidebarright/sandisplay/SanDisplay.gd @@ -1,3 +1,4 @@ +# its really a PGNDisplay but im in no mood to change it extends PanelContainer var tween := Tween.new() @@ -8,12 +9,20 @@ onready var scroll_container := $Scroller onready var scroll_bar: VScrollBar = scroll_container.get_v_scrollbar() onready var sans := $Scroller/sanholder +var added_sans := 0 + func _ready() -> void: + scroll_bar.hide() scroll_bar.step = .15 #smoth add_child(tween) - Utils.connect("newmove", self, "on_new_move") - Utils.connect("pop_move", self, "reset_moves") + if Globals.grid: + Globals.grid.connect("add_to_pgn", self, "add_move") + Globals.grid.connect("clear_pgn", self, "clear") + Globals.grid.connect("remove_last", self, "pop") + else: + for i in "hello how do you do": + add_move(i) func create_number_label(num: int) -> void: @@ -24,23 +33,36 @@ func create_number_label(num: int) -> void: base.name = base.number.text -func add_move_to_label(move: String) -> void: - if !Globals.turn: - create_number_label(Globals.fullmove) +func add_move(move: String) -> void: + if added_sans % 2 == 0: + # warning-ignore:integer_division + create_number_label((added_sans / 2) + 1) + added_sans += 1 sans.get_children()[-1].add_move(move) + scroll_down() -func on_new_move(move: String) -> void: - add_move_to_label(move) +func scroll_down(): tween.interpolate_property(scroll_bar, "value", scroll_bar.value, scroll_bar.max_value, 0.5, 9) # bouncy tween.start() -func reset_moves() -> void: +func clear() -> void: + added_sans = 0 for i in sans.get_children(): - i.queue_free() + i.free() + + +func pop() -> void: + added_sans -= 1 + var cs = sans.get_children() + cs.invert() + for c in cs: + if !c.is_queued_for_deletion(): + c.pop_move() + return -func _gui_input(event: InputEvent) -> void: - if event is InputEventMouseButton: - OS.clipboard = Utils.get_pgn() +func _gui_input(_e: InputEvent) -> void: + if Input.is_action_just_pressed("click") and Globals.grid: + OS.clipboard = Globals.grid.chess.pgn() diff --git a/ui/menus/sidebarright/sandisplay/SanDisplay.tscn b/ui/menus/sidebarright/sandisplay/SanDisplay.tscn index 1a28bdf..a06fe11 100644 --- a/ui/menus/sidebarright/sandisplay/SanDisplay.tscn +++ b/ui/menus/sidebarright/sandisplay/SanDisplay.tscn @@ -15,7 +15,7 @@ Base = ExtResource( 10 ) [node name="Scroller" type="ScrollContainer" parent="."] margin_right = 1422.0 margin_bottom = 800.0 -mouse_filter = 2 +mouse_filter = 1 scroll_horizontal_enabled = false __meta__ = { "_edit_group_": true, @@ -24,5 +24,6 @@ __meta__ = { [node name="sanholder" type="VBoxContainer" parent="Scroller"] margin_right = 1422.0 +mouse_filter = 2 size_flags_horizontal = 3 custom_constants/separation = 0 diff --git a/ui/menus/sidebarright/undobutton.gd b/ui/menus/sidebarright/undobutton.gd index 20244c4..483005a 100644 --- a/ui/menus/sidebarright/undobutton.gd +++ b/ui/menus/sidebarright/undobutton.gd @@ -16,25 +16,27 @@ func _pressed() -> void: if waiting_on_answer: _confirmed(true) else: - if Utils.moves_list.size() == 0: + var two_undos = true if Globals.grid.chess.turn == Globals.team else false + var completed_moves = Globals.grid.chess.history().size() + if completed_moves == 0 or (two_undos && completed_moves == 1): status.set_text("No moves to undo!") return - elif Globals.turn == Globals.team: - status.set_text("It is your turn!") - return - PacketHandler.send_packet({gamecode = PacketHandler.game_code, question = ""}, PacketHandler.HEADERS.undo) - Globals.chat.server(undo_request_message % Globals.get_team()) + var msg = undo_request_message % Utils.expand_color(Globals.team) + var pckt = {gamecode = PacketHandler.game_code, question = msg, two = two_undos} + status.set_text("") + PacketHandler.send_packet(pckt, PacketHandler.HEADERS.undo) + Globals.chat.server(msg) set_disabled(true) func undo_signal_recieved(sig: Dictionary) -> void: if "question" in sig: - Globals.chat.server(undo_request_message % Globals.str_bool(!Globals.team)) + Globals.chat.server(sig.question) confirm() else: set_disabled(false) if sig.accepted: - undo() + undo(sig.two) else: # declined signal reception Globals.chat.server(undo_declined_message) @@ -42,25 +44,16 @@ func undo_signal_recieved(sig: Dictionary) -> void: func _confirmed(what: bool) -> void: ._confirmed(what) - PacketHandler.send_packet({gamecode = PacketHandler.game_code, accepted = what}, PacketHandler.HEADERS.undo) + var two_undos = false if Globals.grid.chess.turn == Globals.team else true + var pckt = {gamecode = PacketHandler.game_code, accepted = what, two = two_undos} + PacketHandler.send_packet(pckt, PacketHandler.HEADERS.undo) if what: - undo() + undo(two_undos) else: # pressed no reception Globals.chat.server(undo_declined_message) -func undo(): - var numberex = SanParse.compile("(^[0-9]+)\\.", false) - var which_move = 0 - var mov = Utils.moves_list[-1] - var result = numberex.search(mov) - if result: - which_move = result.strings[1] - else: - result = numberex.search(Utils.moves_list[-2]) - which_move = result.strings[1] if result else 0 - var pgn = Utils.pop_move() - Globals.chat.server("Move (%s) %s undone" % [which_move, mov.split(" ")[-1]]) - Globals.grid.undo(pgn) +func undo(two_undos := false): + Globals.grid.undo(two_undos) status.set_text("") diff --git a/ui/menus/tests/engine_test.gd b/ui/menus/tests/engine_test.gd new file mode 100644 index 0000000..97173c0 --- /dev/null +++ b/ui/menus/tests/engine_test.gd @@ -0,0 +1,290 @@ +extends Button + + +class TestChess: + extends Resource + + const LOG_FILE = "user://tests.log" + + func test_algebraic_conversion(): + for k in Chess.SQUARE_MAP: + assert(Chess.algebraic(Chess.SQUARE_MAP[k]) == k) + + func test_perft(): + var perfts = [ + { + fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + depth = 3, + nodes = 90884, + }, + {fen = "8/PPP4k/8/8/8/8/4Kppp/8 w - - 0 1", depth = 4, nodes = 84923}, + { + fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", + depth = 4, + nodes = 43238, + }, + { + fen = "rnbqkbnr/p3pppp/2p5/1pPp4/3P4/8/PP2PPPP/RNBQKBNR w KQkq b6 0 4", + depth = 3, + nodes = 23509, + }, + ] + for perft in perfts: + var c = Chess.new(perft.fen) + var nodes = c.perft(perft.depth) + assert(nodes == perft.nodes) + + func test_single_square_move_generation(): + var positions = [ + { + fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + square = "e2", + verbose = false, + moves = ["e3", "e4"], + }, + { + fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + square = "e9", + verbose = false, + moves = [], + }, #invalid square + { + fen = "rnbqk1nr/pppp1ppp/4p3/8/1b1P4/2N5/PPP1PPPP/R1BQKBNR w KQkq - 2 3", + square = "c3", + verbose = false, + moves = [], + }, # pinned piece + { + fen = "8/k7/8/8/8/8/7p/K7 b - - 0 1", + square = "h2", + verbose = false, + moves = ["h1=Q+", "h1=R+", "h1=B", "h1=N"], + }, # promotion + { + fen = "r1bq1rk1/1pp2ppp/p1np1n2/2b1p3/2B1P3/2NP1N2/PPPBQPPP/R3K2R w KQ - 0 8", + square = "e1", + verbose = false, + moves = ["Kf1", "Kd1", "O-O", "O-O-O"], + }, # castling + { + fen = "r1bq1rk1/1pp2ppp/p1np1n2/2b1p3/2B1P3/2NP1N2/PPPBQPPP/R3K2R w - - 0 8", + square = "e1", + verbose = false, + moves = ["Kf1", "Kd1"], + }, # no castling + { + fen = "8/7K/8/8/1R6/k7/1R1p4/8 b - - 0 1", + square = "a3", + verbose = false, + moves = [], + }, # trapped king + { + fen = "8/7K/8/8/1R6/k7/1R1p4/8 b - - 0 1", + square = "d2", + verbose = true, + moves = [ + { + color = "b", + from = "d2", + to = "d1", + flags = "np", + piece = "p", + promotion = "q", + san = "d1=Q", + }, + { + color = "b", + from = "d2", + to = "d1", + flags = "np", + piece = "p", + promotion = "r", + san = "d1=R", + }, + { + color = "b", + from = "d2", + to = "d1", + flags = "np", + piece = "p", + promotion = "b", + san = "d1=B", + }, + { + color = "b", + from = "d2", + to = "d1", + flags = "np", + piece = "p", + promotion = "n", + san = "d1=N", + }, + ], + }, # verbose + { + fen = "rnbqk2r/ppp1pp1p/5n1b/3p2pQ/1P2P3/B1N5/P1PP1PPP/R3KBNR b KQkq - 3 5", + square = "f1", + verbose = true, + moves = [], + }, # issue #30 + ] + for position in positions: + var chess = Chess.new(position.fen) + var cfg = { + square = position.square, + verbose = position.verbose, + } + var moves = chess.moves(cfg) + if position.verbose: + for i in range(len(moves)): + assert( + moves[i].hash() == position.moves[i].hash(), + "%s should have been %s" % [moves[i], position.moves[i]] + ) + else: + assert(moves == position.moves, "%s should have been %s" % [moves, position.moves]) + + func test_checkmates(): + var checkmates = [ + "8/5r2/4K1q1/4p3/3k4/8/8/8 w - - 0 7", + "4r2r/p6p/1pnN2p1/kQp5/3pPq2/3P4/PPP3PP/R5K1 b - - 0 2", + "r3k2r/ppp2p1p/2n1p1p1/8/2B2P1q/2NPb1n1/PP4PP/R2Q3K w kq - 0 8", + "8/6R1/pp1r3p/6p1/P3R1Pk/1P4P1/7K/8 b - - 0 4", + ] + + for fen in checkmates: + var chess = Chess.new(fen) + assert(chess.in_checkmate() == true) + assert(chess.in_draw() == false) + + var no_checkmates = [ + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "1R6/8/8/8/8/8/7R/k6K b - - 0 1", # stalemate + ] + + for fen in no_checkmates: + var chess = Chess.new(fen) + assert(chess.in_checkmate() == false) + + func test_stalemates(): + var stalemates = [ + "1R6/8/8/8/8/8/7R/k6K b - - 0 1", + "8/8/5k2/p4p1p/P4K1P/1r6/8/8 w - - 0 2", + ] + for fen in stalemates: + var chess = Chess.new(fen) + assert(chess.in_stalemate() == true) + assert(chess.in_draw() == true) + + func test_insufficient_material(): + var drawn = [ + "8/8/8/8/8/8/8/k6K w - - 0 1", + "8/2N5/8/8/8/8/8/k6K w - - 0 1", + "8/2b5/8/8/8/8/8/k6K w - - 0 1", + "8/b7/3B4/8/8/8/8/k6K w - - 0 1", + "8/b1B1b1B1/1b1B1b1B/8/8/8/8/1k5K w - - 0 1", + ] + + var not_drawn = [ + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "8/2p5/8/8/8/8/8/k6K w - - 0 1", + "8/b7/B7/8/8/8/8/k6K w - - 0 1", + "8/bB2b1B1/1b1B1b1B/8/8/8/8/1k5K w - - 0 1", + ] + for fen in drawn: + var chess = Chess.new(fen) + assert(chess.insufficient_material() == true) + assert(chess.in_draw() == true) + for fen in not_drawn: + var chess = Chess.new(fen) + assert(chess.insufficient_material() == false) + assert(chess.in_draw() == false) + + func test_threefold_repetition(): + var positions = [ + { + fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + moves = "Nf3 Nf6 Ng1 Ng8 Nf3 Nf6 Ng1 Ng8", + }, + # Fischer - Petrosian, Buenos Aires, 1971 + { + fen = "8/pp3p1k/2p2q1p/3r1P2/5R2/7P/P1P1QP2/7K b - - 2 30", + moves = "Qe5 Qh5 Qf6 Qe2 Re5 Qd3 Rd5 Qe2", + }, + ] + + for position in positions: + var chess = Chess.new(position.fen) + for move in position.moves.split(" "): + assert(chess.in_threefold_repetition() == false) + chess.move(move) + assert(chess.in_threefold_repetition() == true) + assert(chess.in_draw() == true) + + func test_move_generation(): + var positions = [ + { + fen = "7k/3R4/3p2Q1/6Q1/2N1N3/8/8/3R3K w - - 0 1", + moves = """Rd8# Re7 Rf7 Rg7 Rh7# R7xd6 Rc7 Rb7 Ra7 Qf7 Qe8# Qg7# Qg8# Qh7# Q6h6# Q6h5# Q6f5 Q6f6# Qe6 Qxd6 Q5f6# Qe7 Qd8# Q5h6# Q5h5# Qh4# Qg4 Qg3 Qg2 Qg1 Qf4 Qe3 Qd2 Qc1 Q5f5 Qe5+ Qd5 Qc5 Qb5 Qa5 Na5 Nb6 Ncxd6 Ne5 Ne3 Ncd2 Nb2 Na3 Nc5 Nexd6 Nf6 Ng3 Nf2 Ned2 Nc3 Rd2 Rd3 Rd4 Rd5 R1xd6 Re1 Rf1 Rg1 Rc1 Rb1 Ra1 Kg2 Kh2 Kg1""", + }, + { + fen = "1r3k2/P1P5/8/8/8/8/8/R3K2R w KQ - 0 1", + moves = """a8=Q a8=R a8=B a8=N axb8=Q+ axb8=R+ axb8=B axb8=N c8=Q+ c8=R+ c8=B c8=N cxb8=Q+ cxb8=R+ cxb8=B cxb8=N Ra2 Ra3 Ra4 Ra5 Ra6 Rb1 Rc1 Rd1 Kd2 Ke2 Kf2 Kf1 Kd1 Rh2 Rh3 Rh4 Rh5 Rh6 Rh7 Rh8+ Rg1 Rf1+ O-O+ O-O-O""", + }, + { + fen = "5rk1/8/8/8/8/8/2p5/R3K2R w KQ - 0 1", + moves = """Ra2 Ra3 Ra4 Ra5 Ra6 Ra7 Ra8 Rb1 Rc1 Rd1 Kd2 Ke2 Rh2 Rh3 Rh4 Rh5 Rh6 Rh7 Rh8+ Rg1+ Rf1""", + }, + { + fen = "5rk1/8/8/8/8/8/2p5/R3K2R b KQ - 0 1", + moves = """Rf7 Rf6 Rf5 Rf4 Rf3 Rf2 Rf1+ Re8+ Rd8 Rc8 Rb8 Ra8 Kg7 Kf7 c1=Q+ c1=R+ c1=B c1=N""", + }, + { + fen = "r3k2r/p2pqpb1/1n2pnp1/2pPN3/1p2P3/2N2Q1p/PPPB1PPP/R3K2R w KQkq c6 0 2", + moves = """gxh3 Qxf6 Qxh3 Nxd7 Nxf7 Nxg6 dxc6 dxe6 Rg1 Rf1 Ke2 Kf1 Kd1 Rb1 Rc1 Rd1 g3 g4 Be3 Bf4 Bg5 Bh6 Bc1 b3 a3 a4 Qf4 Qf5 Qg4 Qh5 Qg3 Qe2 Qd1 Qe3 Qd3 Na4 Nb5 Ne2 Nd1 Nb1 Nc6 Ng4 Nd3 Nc4 d6 O-O O-O-O""", + }, + { + fen = "k7/8/K7/8/3n3n/5R2/3n4/8 b - - 0 1", + moves = """N2xf3 Nhxf3 Nd4xf3 N2b3 Nc4 Ne4 Nf1 Nb1 Nhf5 Ng6 Ng2 Nb5 Nc6 Ne6 Ndf5 Ne2 Nc2 N4b3 Kb8""", + }, + ] + + for position in positions: + var chess = Chess.new(position.fen) + assert(Array(chess.moves()).sort() == Array(position.moves.split(" ")).sort()) + + func test_random_moves(): + for _i in range(5): # 5 random games + var c = Chess.new() + while c.game_over() == false: + var possible_moves = c.moves() + var mov = possible_moves[randi() % len(possible_moves)] + c.move(mov) + Log.file(LOG_FILE, c.pgn() + "\n--------------------\n") + + func _init(): + 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 move generation tests") + test_single_square_move_generation() + Log.file(LOG_FILE, "starting checkmate tests") + test_checkmates() + Log.file(LOG_FILE, "starting stalemate tests") + test_stalemates() + Log.file(LOG_FILE, "starting insufficient material tests") + test_insufficient_material() + Log.file(LOG_FILE, "starting threefold repetition tests") + test_threefold_repetition() + Log.file(LOG_FILE, "starting move generation tests") + test_move_generation() + Log.file(LOG_FILE, "starting random moves tests") + test_random_moves() # crash testing + Log.file(LOG_FILE, "all tests passed") + + +func _pressed(): + TestChess.new() + Log.debug("all tests passed") diff --git a/PGN/test_pgns.tscn b/ui/menus/tests/test_runner.tscn index cd94e8f..f475858 100644 --- a/PGN/test_pgns.tscn +++ b/ui/menus/tests/test_runner.tscn @@ -1,7 +1,10 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=5 format=2] [ext_resource path="res://ui/theme/main.theme" type="Theme" id=1] -[ext_resource path="res://PGN/test_pgns.gd" type="Script" id=2] +[ext_resource path="res://ui/menus/tests/tests.gd" type="Script" id=2] +[ext_resource path="res://ui/menus/tests/engine_test.gd" type="Script" id=3] + +[sub_resource type="StyleBoxEmpty" id=1] [node name="tests" type="VBoxContainer"] anchor_right = 1.0 @@ -11,39 +14,73 @@ script = ExtResource( 2 ) pgns = PoolStringArray( "1.Nf3 Nf6 2.c4 g6 3.Nc3 Bg7 4.d4 O-O 5.Bf4 d5 6.Qb3 dxc4 7.Qxc4 c6 8.e4 Nbd7 9.Rd1 Nb6 10.Qc5 Bg4 11.Bg5 Na4 12.Qa3 Nxc3 13.bxc3 Nxe4 14.Bxe7 Qb6 15.Bc4 Nxc3 16.Bc5 Rfe8+ 17.Kf1 Be6 18.Bxb6 Bxc4+ 19.Kg1 Ne2+ 20.Kf1 Nxd4+ 21.Kg1 Ne2+ 22.Kf1 Nc3+ 23.Kg1 axb6 24.Qb4 Ra4 25.Qxb6 Nxd1 26.h3 Rxa2 27.Kh2 Nxf2 28.Re1 Rxe1 29.Qd8+ Bf8 30.Nxe1 Bd5 31.Nf3 Ne4 32.Qb8 b5 33.h4 h5 34.Ne5 Kg7 35.Kg1 Bc5+ 36.Kf1 Ng3+ 37.Ke1 Bb4+ 38.Kd1 Bb3+ 39.Kc1 Ne2+ 40.Kb1 Nc3+ 41.Kc1 Rc2# 0-1", "1. e4 e5 2. Nf3 Nc6 3. Bb5 Bc5 4. O-O Nf6 5. Nc3 Nb4 6. Nxe5 c6 7. Bc4 d5 8. exd5 cxd5 9. Bb5+ Bd7 10. Bxd7+ Nxd7 11. Nxd7 Qxd7 12. Re1+ Be7 13. a3 Nc6 14. d4 O-O-O 15. h3 Bh4 16. Qh5 Bf6 17. Nb5 Nxd4 18. Nxa7+ Kb8 19. Bg5 Nxc2 20. Bxf6 gxf6 21. a4 Kxa7 22. Qe2 Nxe1 23. Qxe1 Rhe8 24. Qa5+ Kb8 25. Qb4 d4 26. a5 Re6", "1. e4 f5 2. d4 g5 3. Qh5#" ) [node name="gameofthecentury" type="Button" parent="."] -margin_left = 489.0 -margin_right = 932.0 -margin_bottom = 102.0 +margin_left = 517.0 +margin_right = 904.0 +margin_bottom = 46.0 focus_mode = 0 size_flags_horizontal = 4 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 1 ) +custom_styles/focus = SubResource( 1 ) +custom_styles/disabled = SubResource( 1 ) +custom_styles/normal = SubResource( 1 ) text = "game of the century" [node name="test sign" type="Button" parent="."] -margin_left = 599.0 -margin_top = 117.0 -margin_right = 822.0 -margin_bottom = 219.0 +margin_left = 646.0 +margin_top = 61.0 +margin_right = 775.0 +margin_bottom = 107.0 focus_mode = 0 size_flags_horizontal = 4 -text = "test sign" +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 1 ) +custom_styles/focus = SubResource( 1 ) +custom_styles/disabled = SubResource( 1 ) +custom_styles/normal = SubResource( 1 ) +text = "ﭧ sign" [node name="test mate" type="Button" parent="."] -margin_left = 590.0 -margin_top = 234.0 -margin_right = 831.0 -margin_bottom = 336.0 +margin_left = 637.0 +margin_top = 122.0 +margin_right = 784.0 +margin_bottom = 168.0 focus_mode = 0 size_flags_horizontal = 4 -text = "test mate" +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 1 ) +custom_styles/focus = SubResource( 1 ) +custom_styles/disabled = SubResource( 1 ) +custom_styles/normal = SubResource( 1 ) +text = "ﭧ mate" [node name="test chat" type="Button" parent="."] -margin_left = 643.0 -margin_top = 351.0 -margin_right = 779.0 -margin_bottom = 453.0 +margin_left = 671.0 +margin_top = 183.0 +margin_right = 751.0 +margin_bottom = 229.0 size_flags_horizontal = 4 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 1 ) +custom_styles/focus = SubResource( 1 ) +custom_styles/disabled = SubResource( 1 ) +custom_styles/normal = SubResource( 1 ) text = "ﭧ" +[node name="engine tests" type="Button" parent="."] +margin_left = 555.0 +margin_top = 244.0 +margin_right = 867.0 +margin_bottom = 290.0 +size_flags_horizontal = 4 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 1 ) +custom_styles/focus = SubResource( 1 ) +custom_styles/disabled = SubResource( 1 ) +custom_styles/normal = SubResource( 1 ) +text = "run engine tests" +script = ExtResource( 3 ) + [connection signal="pressed" from="gameofthecentury" to="." method="_load" binds= [ 0 ]] [connection signal="pressed" from="test sign" to="." method="_load" binds= [ 1 ]] [connection signal="pressed" from="test mate" to="." method="_load" binds= [ 2 ]] diff --git a/PGN/test_pgns.gd b/ui/menus/tests/tests.gd index f6d76ee..124b0d0 100644 --- a/PGN/test_pgns.gd +++ b/ui/menus/tests/tests.gd @@ -15,7 +15,7 @@ func _load(i: int): var boar = load("res://Game.tscn").instance() get_tree().get_root().add_child(boar) boar = boar.get_board() - boar.play_pgn(pgns[i], true) + boar.load_pgn(pgns[i]) get_parent().hide() diff --git a/ui/theme/main.theme b/ui/theme/main.theme Binary files differindex bc17bdd..4734573 100644 --- a/ui/theme/main.theme +++ b/ui/theme/main.theme |