online multiplayer chess game (note server currently down)
Diffstat (limited to 'board/chess.gd')
| -rw-r--r-- | board/chess.gd | 1248 |
1 files changed, 0 insertions, 1248 deletions
diff --git a/board/chess.gd b/board/chess.gd deleted file mode 100644 index fe60e25..0000000 --- a/board/chess.gd +++ /dev/null @@ -1,1248 +0,0 @@ -extends Resource -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 -static 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 m in moves: - ambig_from = m.from - ambig_to = m.to - ambig_piece = m.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 - - -# WARNING: If `localized` is disabled, you must use localize_piece_move on the returned piece object. -func piece_moves(_square_: String, piece: String, color := turn, localize := true) -> Array: - var moves := [] - var second_rank := {b = RANK_7, w = RANK_2} - var sq: int = SQUARE_MAP[_square_] - if piece == PAWN: - # single square, non capturing - var square = sq + PAWN_OFFSETS[color][0] - if square <= SQUARE_MAP.h1: - __add_piece_move(moves, sq, square, piece) - - # double square - var _square: int = sq + PAWN_OFFSETS[color][1] - if second_rank[color] == rank(sq): - __add_piece_move(moves, sq, _square, piece, BITS.BIG_PAWN) - - # pawn captures - for j in range(2, 4): - var _square: int = sq + PAWN_OFFSETS[color][j] - if _square & 0x88: # off the board - continue - __add_piece_move(moves, sq, _square, piece, BITS.CAPTURE) - else: - for offset in PIECE_OFFSETS[piece]: - var square := sq - while true: - square += offset - if square & 0x88: - break - - __add_piece_move(moves, sq, square, piece) - if piece in KNIGHT + KING: - break - - if piece == KING and _square_ in ["e1", "e8"]: - __add_piece_move(moves, sq, sq + 2, piece, BITS.KSIDE_CASTLE) - __add_piece_move(moves, sq, sq - 2, piece, BITS.QSIDE_CASTLE) - - if localize: - for i in range(len(moves)): - moves[i] = localize_piece_move(moves[i]) - return moves - - -static func __add_piece_move(moves: Array, from: int, to: int, piece: String, flags := BITS.NORMAL) -> void: - var mov = {from = from, to = to, flags = flags} - if piece == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1): - for p in [QUEEN, ROOK, BISHOP, KNIGHT]: - var m = mov.duplicate() - m["promotion"] = p - m.flags |= BITS.PROMOTION - moves.append(m) - else: - moves.append(mov) - - -# Please only use the returned object if the team the move was created for is the current turn. -func localize_piece_move(piece_move: Dictionary) -> Dictionary: - return __build_move(piece_move.from, piece_move.to, piece_move.flags, piece_move.get("promotion", "")) - - -### -### utility functions -### -static func rank(i: int) -> int: - return i >> 4 - - -static func file(i: int) -> int: - return i & 15 - - -static func vecfrom0x88(i: int) -> Vector2: - return Vector2(file(i), rank(i)) - - -static func vec2algebraic(pos: Vector2) -> String: - var column := "abcdefgh"[pos.x] - var row := str(round(8 - pos.y)) - 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 vec2algebraic(vecfrom0x88(pos) + offset) - 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 + 1: - 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 - - -func __add_move(moves: Array, from: int, to: int, flags := BITS.NORMAL, b := board) -> void: - # if pawn promotion - if b[from].type == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1): - for p in [QUEEN, ROOK, BISHOP, KNIGHT]: - moves.append(__build_move(from, to, flags, p)) - else: - moves.append(__build_move(from, to, flags)) - - -func __build_move(from: int, to: int, flags: int = BITS.NORMAL, promotion := "", _board: Array = board): - var move := { - color = turn, - from = from, - to = to, - flags = flags, - piece = _board[from].type, - } - - 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 - - -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 - 1 - 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(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(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(moves, i, _square, BITS.CAPTURE) - elif _square == ep_square: - __add_move(moves, i, ep_square, BITS.EP_CAPTURE) - elif piece_type == "-1" || piece_type == piece.type: - for offset in PIECE_OFFSETS[piece.type]: - var square := i - - while true: - square += offset - if square & 0x88: - break - - if board[square] == null: - __add_move(moves, i, square, BITS.NORMAL) - else: - if board[square].color == us: - break - __add_move(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(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(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 := -1 - 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 := -1 - 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: - board[move.to + (-16 if us == BLACK else 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 - ep_square = (move.to + (-16 if turn == BLACK else 16)) if move.flags & BITS.BIG_PAWN else EMPTY - - # reset the 50 move counter if a pawn is moved or a piece is captured - half_moves = 0 if move.piece == PAWN or move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE) else (half_moves + 1) - - fullmoves += 1 if turn == BLACK else 0 - 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 - 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: - board[move.to + (-16 if us == BLACK else 16)] = {type = PAWN, color = them} - - if move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE): - var castling_to - 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 and 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)) - elif "stripped" in options && options.stripped: - moves.append(stripped_san(__move_to_san(ugly_move, __generate_moves({legal = true}), false))) - 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 := -1 - 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) - - if move_string.length(): - moves.append(move_string) - - # __history should be back to what it was before we started generating PGN, - # so join together moves - return moves.join(" ") - - -func load_pgn(pgn: String, options := {}) -> int: - # 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 |