online multiplayer chess game (note server currently down)
Diffstat (limited to 'board/chess.gd')
-rw-r--r--board/chess.gd1248
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