stockfish for godot
update
bendn 2022-09-13
parent 79eb3aa · commit e576e2c
-rw-r--r--.eslintrc.yml9
-rw-r--r--Main.gd4
-rw-r--r--addons/stockfish.gd/chess.gd1293
-rw-r--r--addons/stockfish.gd/fen.gd35
-rw-r--r--addons/stockfish.gd/load.js2
-rw-r--r--addons/stockfish.gd/package.json2
-rw-r--r--addons/stockfish.gd/pgn.gd53
-rw-r--r--addons/stockfish.gd/stockfish_loader.gd74
-rwxr-xr-xexport_html.sh18
-rw-r--r--project.godot18
10 files changed, 1483 insertions, 25 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
index 0000000..b087254
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,9 @@
+env:
+ browser: true
+ commonjs: true
+ es2021: true
+extends: eslint:recommended
+overrides: []
+parserOptions:
+ ecmaVersion: latest
+rules: {}
diff --git a/Main.gd b/Main.gd
index 71cc0b9..8287081 100644
--- a/Main.gd
+++ b/Main.gd
@@ -7,4 +7,6 @@ func _ready() -> void:
var loader := StockfishLoader.new()
fish = loader.load_stockfish()
yield(fish, "engine_ready")
- fish.run_command("go depth 5")
+ print("GO FISH")
+ fish.game = Chess.new()
+ fish.go()
diff --git a/addons/stockfish.gd/chess.gd b/addons/stockfish.gd/chess.gd
new file mode 100644
index 0000000..b84b736
--- /dev/null
+++ b/addons/stockfish.gd/chess.gd
@@ -0,0 +1,1293 @@
+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",
+}
+
+var fen_parser := FEN.new()
+var pgn_parser := PGN.new()
+
+
+# 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_parser.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
+) -> Dictionary:
+ 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 __move_from_uci(uci: String) -> Dictionary:
+ if len(uci) <= 5:
+ var flags = BITS.NORMAL
+ if board[SQUARE_MAP[uci.substr(2, 4)]]:
+ flags = BITS.CAPTURE
+ return __build_move(
+ SQUARE_MAP[uci.substr(0, 2)],
+ SQUARE_MAP[uci.substr(2, 4)],
+ flags,
+ uci[5] if len(uci) == 5 else ""
+ )
+
+ 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_parser.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('e2e4') <- where 'move' is a uci 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)
+ if !move_obj:
+ move_obj = __move_from_uci(move)
+ 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
+
+
+static func move_to_uci(move: Dictionary) -> String:
+ if move.promotion:
+ return algebraic(move.from) + algebraic(move.to) + move.promotion
+ else:
+ return algebraic(move.from) + algebraic(move.to)
+
+
+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/addons/stockfish.gd/fen.gd b/addons/stockfish.gd/fen.gd
new file mode 100644
index 0000000..7da99c2
--- /dev/null
+++ b/addons/stockfish.gd/fen.gd
@@ -0,0 +1,35 @@
+extends Reference
+class_name FEN
+
+var reg: = RegEx.new()
+var reg_src:= "^(?<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 _init() -> void:
+ reg.compile(reg_src)
+
+func parse(fen: String) -> Dictionary:
+ var res = reg.search(fen)
+ if res:
+ var mat: Array = []
+ var rows = res.strings[res.names.pieces].split("/")
+ for row in rows:
+ var append_row: Array = []
+ for col in row:
+ if int(col) != 0:
+ for _i in range(int(col)):
+ append_row.append("")
+ else:
+ append_row.append(col)
+ mat.append(append_row)
+ var fenobj = {
+ "mat": mat,
+ "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]),
+ "fullmove": int(res.strings[res.names.fullmove])
+ }
+ return fenobj
+ else:
+ push_error("bad fen")
+ return {}
diff --git a/addons/stockfish.gd/load.js b/addons/stockfish.gd/load.js
index 1b9f90f..16961c5 100644
--- a/addons/stockfish.gd/load.js
+++ b/addons/stockfish.gd/load.js
@@ -1 +1 @@
-const dl=(url,onFinishDownload)=>{fetch(url).then(response=>response.arrayBuffer()).then(data=>onFinishDownload(data))};window.stockfish=null;let stockfish_state='LOADING';let output='';window.stockfishCommand=function(command){window.stockfish.postMessage(command)};const loadStockfish=async params=>{return await Stockfish(params)};const onFinishDownload=data=>{if(!data){window.stockfish_state='FAILED';window.stockfish_failed_load();return}loadStockfish({wasmBinary:data}).then(_stockfish=>{window.stockfish=_stockfish;window.stockfish_state='READY';window.stockfish.addMessageListener(line=>window.stockfish_data_recieved(line));window.stockfish_ready()}).catch(e=>{window.stockfish_state='FAILED';window.stockfish_failed_load();throw e})};dl('./lib/stockfish.wasm',onFinishDownload);
+const dl=(url,onFinishDownload)=>{fetch(url).then(response=>response.arrayBuffer()).then(data=>onFinishDownload(data))};window.stockfishCommand=function(command){window.stockfish.postMessage(command)};const loadStockfish=async params=>{return await Stockfish(params)};const onFinishDownload=data=>{if(!data){window.stockfish_failed_load();return}loadStockfish({wasmBinary:data}).then(_stockfish=>{window.stockfish=_stockfish;window.stockfish.addMessageListener(line=>window.stockfish_data_recieved(line))}).catch(e=>{window.stockfish_failed_load();throw e})};dl("./lib/stockfish.wasm",onFinishDownload);
diff --git a/addons/stockfish.gd/package.json b/addons/stockfish.gd/package.json
index 128c3e2..6668326 100644
--- a/addons/stockfish.gd/package.json
+++ b/addons/stockfish.gd/package.json
@@ -1,6 +1,6 @@
{
"name": "@bendn/stockfish.gd",
- "version": "1.0.1",
+ "version": "1.1.0",
"description": "godot stockfish",
"main": "stockfish_loader.gd",
"scripts": {
diff --git a/addons/stockfish.gd/pgn.gd b/addons/stockfish.gd/pgn.gd
new file mode 100644
index 0000000..21a4ea8
--- /dev/null
+++ b/addons/stockfish.gd/pgn.gd
@@ -0,0 +1,53 @@
+extends Reference
+class_name PGN
+
+var movetextex = 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 = compile('^\\[([A-Za-z0-9_]+)\\s+"([^\\r]*)"\\]\\s*$')
+var tagnameex = compile("^[A-Za-z0-9_]+\\Z")
+
+static func compile(src: String) -> RegEx:
+ var regex := RegEx.new()
+ regex.compile(src)
+ return regex
+
+
+func parse(pgn: String, tags := true) -> Dictionary:
+ # put tags into a dictionary,
+ # and the moves into a array
+ var lines = Array(pgn.split("\n"))
+ var headers := {}
+ if tags:
+ # get headers
+ while !lines.empty():
+ var line = lines[0].strip_edges()
+ if !line or line[0] in ["%", ";"]:
+ lines.pop_front()
+ continue
+
+ if line[0] != "[":
+ break
+
+ lines.pop_front()
+ var tag_match = tagex.search(line)
+ if tag_match:
+ var cap = tag_match.strings
+ if tagnameex.search(cap[1]):
+ headers[cap[1]] = cap[2]
+ else:
+ # invalid headers
+ return {}
+ else:
+ break
+ var movetext := PoolStringArray()
+ while !lines.empty():
+ var line = lines.pop_front().strip_edges()
+ if !line:
+ break
+ if line[0] in ["%", ";"]:
+ continue
+ for found in movetextex.search_all(line):
+ if found.strings[1]:
+ movetext.append(found.strings[1])
+ return {"headers": headers, "moves": movetext}
diff --git a/addons/stockfish.gd/stockfish_loader.gd b/addons/stockfish.gd/stockfish_loader.gd
index a6df12f..126cbe9 100644
--- a/addons/stockfish.gd/stockfish_loader.gd
+++ b/addons/stockfish.gd/stockfish_loader.gd
@@ -25,12 +25,62 @@ func is_supported() -> bool:
class Stockfish:
extends Reference
+ var game: Chess setget set_game
+ var sent_isready := false
+
signal engine_ready
- signal data_received
+ signal line_recieved
signal load_failed
- func run_command(cmd: String) -> void:
- pass
+ # @override
+ func send_line(cmd: String) -> void:
+ print("%s --> stockfish" % cmd)
+
+ func _init() -> void:
+ connect("line_recieved", self, "_line_recieved")
+
+ func set_game(new_game: Chess) -> void:
+ game = new_game
+ send_line("ucinewgame")
+ _position()
+
+ func _position():
+ var command := PoolStringArray(["position", "startpos"])
+
+ if game.__history:
+ command.append("moves")
+ for move in game.__history:
+ command.append(Chess.move_to_uci(move))
+
+ send_line(command.join(" "))
+
+ func _line_recieved(line: String) -> void:
+ if line.begins_with("info "):
+ prints("(stockfish)", line)
+ elif line.begins_with("bestmove "):
+ parse_bestmove(line.split(" ", true, 1)[1])
+ elif (sent_isready) && (line == "readyok" || line.begins_with("Stockfish [commit: ")):
+ sent_isready = false
+ emit_signal("engine_ready")
+ else:
+ push_error("unexpected output: %s" % line)
+
+ func parse_bestmove(args: String) -> void:
+ var tokens = args.split(" ")
+ if tokens and not tokens[0] in ["(none)", "NULL"]:
+ if game.move(tokens[0]):
+ var bm = game.undo()
+ emit_signal("bestmove", bm)
+ emit_signal("bestmove", null)
+
+ func go(depth: int = 15):
+ var command := PoolStringArray(["go"])
+ command.append("depth")
+ command.append(str(depth))
+ send_line(command.join(" "))
+
+ func stop():
+ send_line("stop")
class JSStockfish:
@@ -38,27 +88,21 @@ class JSStockfish:
var data_recieved_callback := JavaScript.create_callback(self, "data_recieved")
var load_failed_callback := JavaScript.create_callback(self, "load_failed")
- var ready_callback := JavaScript.create_callback(self, "ready")
func _init() -> void:
+ sent_isready = true
JavaScript.get_interface("window").stockfish_data_recieved = data_recieved_callback
JavaScript.get_interface("window").stockfish_failed_load = load_failed_callback
- JavaScript.get_interface("window").stockfish_ready = ready_callback
- func run_command(cmd: String) -> void:
+ func send_line(cmd: String) -> void:
+ .send_line(cmd)
JavaScript.eval("window.stockfishCommand('%s')" % cmd)
# js callback arguments are in arrays. i guess its so that you can call functions with less args then they want?
- func data_recieved(data:Array) -> void:
- emit_signal("data_received", data[0])
- print(data[0])
+ func data_recieved(data: Array) -> void:
+ emit_signal("line_recieved", data[0])
# if _data is omitted, it will not work
- func load_failed(_data:Array) -> void:
+ func load_failed(_data: Array) -> void:
emit_signal("load_failed")
printerr("load failed")
-
- # ditto
- func ready(_data:Array) -> void:
- emit_signal("engine_ready")
-
diff --git a/export_html.sh b/export_html.sh
index 0477477..6fb369d 100755
--- a/export_html.sh
+++ b/export_html.sh
@@ -3,17 +3,21 @@
set -e
function install_libs() {
- mkdir lib/
- wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.js" -O lib/stockfish.js &
- wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.worker.js" -O lib/stockfish.worker.js &
- wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.wasm" -O lib/stockfish.wasm &
- wget -nv "https://raw.githubusercontent.com/hi-ogawa/stockfish-nnue-wasm-demo/master/public/serve.json" -O serve.json &
- wait
+ if [[ ! -d /tmp/stockfish_libs ]]; then
+ mkdir /tmp/stockfish_libs
+ wget -nv "https://raw.githubusercontent.com/hi-ogawa/stockfish-nnue-wasm-demo/master/public/serve.json" -O /tmp/serve.json &
+ wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.js" -O /tmp/stockfish_libs/stockfish.js &
+ wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.worker.js" -O /tmp/stockfish_libs/stockfish.worker.js &
+ wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.wasm" -O /tmp/stockfish_libs/stockfish.wasm &
+ wait
+ fi
+ cp /tmp/serve.json serve.json
+ cp -r /tmp/stockfish_libs/ lib/
}
[[ -d exports ]] && rm -rf exports
mkdir exports
-[[ -f web/load.js ]] && uglifyjs web/load.js | tr "\"" "'" >addons/stockfish.gd/load.js
+[[ -f web/load.js ]] && uglifyjs web/load.js >addons/stockfish.gd/load.js
godot --no-window --export "HTML5" exports/index.html
cd exports
install_libs
diff --git a/project.godot b/project.godot
index 9dd90b6..9510d9c 100644
--- a/project.godot
+++ b/project.godot
@@ -9,12 +9,30 @@
config_version=4
_global_script_classes=[ {
+"base": "Resource",
+"class": "Chess",
+"language": "GDScript",
+"path": "res://addons/stockfish.gd/chess.gd"
+}, {
+"base": "Reference",
+"class": "FEN",
+"language": "GDScript",
+"path": "res://addons/stockfish.gd/fen.gd"
+}, {
+"base": "Reference",
+"class": "PGN",
+"language": "GDScript",
+"path": "res://addons/stockfish.gd/pgn.gd"
+}, {
"base": "Reference",
"class": "StockfishLoader",
"language": "GDScript",
"path": "res://addons/stockfish.gd/stockfish_loader.gd"
} ]
_global_script_class_icons={
+"Chess": "",
+"FEN": "",
+"PGN": "",
"StockfishLoader": ""
}