stockfish for godot
update
| -rw-r--r-- | .eslintrc.yml | 9 | ||||
| -rw-r--r-- | Main.gd | 4 | ||||
| -rw-r--r-- | addons/stockfish.gd/chess.gd | 1293 | ||||
| -rw-r--r-- | addons/stockfish.gd/fen.gd | 35 | ||||
| -rw-r--r-- | addons/stockfish.gd/load.js | 2 | ||||
| -rw-r--r-- | addons/stockfish.gd/package.json | 2 | ||||
| -rw-r--r-- | addons/stockfish.gd/pgn.gd | 53 | ||||
| -rw-r--r-- | addons/stockfish.gd/stockfish_loader.gd | 74 | ||||
| -rwxr-xr-x | export_html.sh | 18 | ||||
| -rw-r--r-- | project.godot | 18 |
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: {} @@ -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": "" } |