online multiplayer chess game (note server currently down)
STOCKFISH 
bendn 2022-09-14
parent b483fcd · commit 97a56fe
-rwxr-xr-x.github/post_export9
-rw-r--r--FEN/Fen.gd34
-rw-r--r--Globals.gd2
-rw-r--r--Log.gd1
-rw-r--r--PGN/PGN.gd48
-rw-r--r--Utils.gd3
-rw-r--r--board/chess.gd1248
-rw-r--r--export_presets.cfg6
-rw-r--r--godot.lock4
-rw-r--r--godot.package3
-rw-r--r--networking/PacketHandler.gd5
-rw-r--r--project.godot47
-rw-r--r--ui/board/Board.gd25
-rw-r--r--ui/board/Game.gd4
-rw-r--r--ui/chat/Chat.gd11
-rw-r--r--ui/menus/LocalMultiplayer.gd43
-rw-r--r--ui/menus/lobby/GameConfig.gd16
-rw-r--r--ui/menus/lobby/GameConfig.tscn128
-rw-r--r--ui/menus/lobby/Lobby.gd12
-rw-r--r--ui/menus/lobby/Lobby.tscn2
-rw-r--r--ui/menus/lobby/PGNEntry.gd10
-rw-r--r--ui/menus/local_multiplayer/EngineDepth.gd21
-rw-r--r--ui/menus/local_multiplayer/EngineDepthSlider.tscn43
-rw-r--r--ui/menus/local_multiplayer/GameConfig.gd27
-rw-r--r--ui/menus/local_multiplayer/GameConfig.tscn158
-rw-r--r--ui/menus/local_multiplayer/LocalMultiplayer.gd115
-rw-r--r--ui/menus/local_multiplayer/LocalMultiplayer.tscn (renamed from ui/menus/LocalMultiplayer.tscn)13
-rw-r--r--ui/menus/local_multiplayer/NoEngineLabel.gd27
-rw-r--r--ui/menus/local_multiplayer/PlayerOptionButton.tscn13
-rw-r--r--ui/menus/local_multiplayer/color.tres3
-rw-r--r--ui/menus/startmenu/StartMenu.tscn6
-rw-r--r--ui/theme/main.themebin1943 -> 2357 bytes
-rw-r--r--ui/theme/scrollbar/scroll.tres4
-rw-r--r--ui/theme/slider/grabber.pngbin0 -> 184 bytes
-rw-r--r--ui/theme/slider/grabber.png.import35
-rw-r--r--ui/theme/slider/grabber_highlight.pngbin0 -> 187 bytes
-rw-r--r--ui/theme/slider/grabber_highlight.png.import35
37 files changed, 656 insertions, 1505 deletions
diff --git a/.github/post_export b/.github/post_export
new file mode 100755
index 0000000..1424df9
--- /dev/null
+++ b/.github/post_export
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+if [[ $1 == "web" ]]; then
+ mkdir -p build/web/lib
+ wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.js" -O build/web/lib/stockfish.js &
+ wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.worker.js" -O build/web/lib/stockfish.worker.js &
+ wget -nv "https://cdn.jsdelivr.net/npm/stockfish-nnue.wasm/stockfish.wasm" -O build/web/lib/stockfish.wasm &
+ wait
+fi
diff --git a/FEN/Fen.gd b/FEN/Fen.gd
deleted file mode 100644
index ef59e7f..0000000
--- a/FEN/Fen.gd
+++ /dev/null
@@ -1,34 +0,0 @@
-extends Node
-class_name FEN
-
-var reg = Utils.compile(
- "^(?<pieces>([pnbrqkPNBRQK1-8]{1,8}/?){8})\\s+(?<turn>b|w)\\s+(?<castling>-|K?Q?k?q?)\\s+(?<enpassant>-|[a-h][3-6])\\s+(?<halfmove>\\d+)\\s+(?<fullmove>\\d+)"
-)
-
-
-func parse(fen: String) -> Dictionary:
- var 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:
- Log.err("bad fen")
- return {}
diff --git a/Globals.gd b/Globals.gd
index 51e0512..232a243 100644
--- a/Globals.gd
+++ b/Globals.gd
@@ -5,6 +5,7 @@ var piece_set := "california"
var board_color1: Color = Color(0.870588, 0.890196, 0.901961)
var board_color2: Color = Color(0.54902, 0.635294, 0.678431)
var spectating := false
+var local := false
var playing := false setget , get_playing
var chat: Chat = null
var grid: Grid = null
@@ -14,6 +15,7 @@ func reset_vars() -> void:
team = "w"
grid = null
chat = null
+ local = false
spectating = false
diff --git a/Log.gd b/Log.gd
index e3e37d4..e43c3bd 100644
--- a/Log.gd
+++ b/Log.gd
@@ -1,4 +1,5 @@
extends Node
+class_name Log
# static class
diff --git a/PGN/PGN.gd b/PGN/PGN.gd
deleted file mode 100644
index 9711f10..0000000
--- a/PGN/PGN.gd
+++ /dev/null
@@ -1,48 +0,0 @@
-extends Node
-class_name PGN
-
-var movetextex = Utils.compile(
- "([NBKRQ]?[a-h]?[1-8]?[\\-x]?[a-h][1-8](?:=?[nbrqkNBRQK])?|[PNBRQK]?@[a-h][1-8]|--|Z0|0000|@@@@|O-O(?:-O)?|0-0(?:-0)?)|(\\{.*)|(;.*)|(\\$[0-9]+)|(\\()|(\\))|(\\*|1-0|0-1|1\\/2-1\\/2)|([\\?!]{1,2})"
-)
-var tagex = Utils.compile('^\\[([A-Za-z0-9_]+)\\s+"([^\\r]*)"\\]\\s*$')
-var tagnameex = Utils.compile("^[A-Za-z0-9_]+\\Z")
-
-
-func parse(pgn: String, tags := true) -> 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/Utils.gd b/Utils.gd
index a4930f4..156313d 100644
--- a/Utils.gd
+++ b/Utils.gd
@@ -128,7 +128,8 @@ func cli() -> void:
if args.has("host") and args.host:
if PacketHandler.lobby.validate_text(args.host):
var pgn_input = args.get("moves", PoolStringArray()).join(" ")
- var move_list = Pgn.parse(pgn_input, false).moves
+ var pgn_parser = PGN.new()
+ var move_list = pgn_parser.parse(pgn_input, false).moves
var clr = str_bool(args.color, ["w", "white"]) if args.has("color") else true # default white
var string = "hosting game: %s" % args.host
string += ", with moves: %s" % move_list if move_list else ""
diff --git a/board/chess.gd b/board/chess.gd
deleted file mode 100644
index fe60e25..0000000
--- a/board/chess.gd
+++ /dev/null
@@ -1,1248 +0,0 @@
-extends Resource
-class_name Chess
-# ported from https://github.com/jhlywa/chess.js
-const SYMBOLS := "pnbrqkPNBRQK"
-
-const DEFAULT_POSITION := "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
-
-const TERMINATION_MARKERS := ["1-0", "0-1", "1/2-1/2", "*"]
-
-const PAWN_OFFSETS := {
- b = [16, 32, 17, 15],
- w = [-16, -32, -17, -15],
-}
-
-const PIECE_OFFSETS := {
- n = [-18, -33, -31, -14, 18, 33, 31, 14],
- b = [-17, -15, 17, 15],
- r = [-16, 1, 16, -1],
- q = [-17, -16, -15, 1, 17, 16, 15, -1],
- k = [-17, -16, -15, 1, 17, 16, 15, -1],
-}
-
-var ATTACKS: PoolIntArray = [
- 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0,
- 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
- 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
- 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
- 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
- 24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0,
- 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
- 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
- 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
- 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
- 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20
-]
-
-var RAYS: PoolIntArray = [
- 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
- 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
- 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
- 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
- 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
- 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0,
- 0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0,
- 0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0,
- 0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0,
- 0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0,
- -15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17
-]
-
-const SQUARE_MAP := {
- a8 = 0, b8 = 1, c8 = 2, d8 = 3, e8 = 4, f8 = 5, g8 = 6, h8 = 7,
- a7 = 16, b7 = 17, c7 = 18, d7 = 19, e7 = 20, f7 = 21, g7 = 22, h7 = 23,
- a6 = 32, b6 = 33, c6 = 34, d6 = 35, e6 = 36, f6 = 37, g6 = 38, h6 = 39,
- a5 = 48, b5 = 49, c5 = 50, d5 = 51, e5 = 52, f5 = 53, g5 = 54, h5 = 55,
- a4 = 64, b4 = 65, c4 = 66, d4 = 67, e4 = 68, f4 = 69, g4 = 70, h4 = 71,
- a3 = 80, b3 = 81, c3 = 82, d3 = 83, e3 = 84, f3 = 85, g3 = 86, h3 = 87,
- a2 = 96, b2 = 97, c2 = 98, d2 = 99, e2 = 100, f2 = 101, g2 = 102, h2 = 103,
- a1 = 112, b1 = 113, c1 = 114, d1 = 115, e1 = 116, f1 = 117, g1 = 118, h1 = 119
-}
-
-const SHIFTS := {p = 0, n = 1, b = 2, r = 3, q = 4, k = 5}
-
-const BITS := {
- NORMAL = 1,
- CAPTURE = 2,
- BIG_PAWN = 4, # 2 step pawn
- EP_CAPTURE = 8,
- PROMOTION = 16,
- KSIDE_CASTLE = 32,
- QSIDE_CASTLE = 64,
-}
-
-const ROOKS := {
- w = [
- {square = SQUARE_MAP.a1, flag = BITS.QSIDE_CASTLE},
- {square = SQUARE_MAP.h1, flag = BITS.KSIDE_CASTLE},
- ],
- b = [
- {square = SQUARE_MAP.a8, flag = BITS.QSIDE_CASTLE},
- {square = SQUARE_MAP.h8, flag = BITS.KSIDE_CASTLE},
- ],
-}
-enum { RANK_8, RANK_7, RANK_6, RANK_5, RANK_4, RANK_3, RANK_2, RANK_1 }
-enum { PARSER_STRICT, PARSER_SLOPPY }
-const BLACK := "b"
-const WHITE := "w"
-const EMPTY := -1
-const PAWN := "p"
-const KNIGHT := "n"
-const BISHOP := "b"
-const ROOK := "r"
-const QUEEN := "q"
-const KING := "k"
-const FLAGS := {
- NORMAL = "n",
- CAPTURE = "c",
- BIG_PAWN = "b",
- EP_CAPTURE = "e",
- PROMOTION = "p",
- KSIDE_CASTLE = "k",
- QSIDE_CASTLE = "q",
-}
-
-
-# parses all of the decorators out of a SAN string
-static func stripped_san(move: String) -> String:
- var reg := RegEx.new()
- reg.compile("(\\+|\\#)?(\\?\\?|\\?|\\?!|!|!!)?$")
- return reg.sub(move.replace("=", ""), "")
-
-
-# this func is used to uniquely identify ambiguous moves
-static func get_disambiguator(move: Dictionary, moves: Array) -> String:
- var from: int = move.from
- var to: int = move.to
- var piece: String = move.piece
-
- var ambiguities := 0
- var same_rank := 0
- var same_file := 0
- var ambig_piece: String
- var ambig_to: int
- var ambig_from: int
- for m in moves:
- ambig_from = m.from
- ambig_to = m.to
- ambig_piece = m.piece
-
- # if a move of the same piece type ends on the same to square, we'll
- # need to add a disambiguator to the algebraic notation
- if piece == ambig_piece && from != ambig_from && to == ambig_to:
- ambiguities += 1
-
- if rank(from) == rank(ambig_from):
- same_rank += 1
-
- if file(from) == file(ambig_from):
- same_file += 1
-
- if ambiguities > 0:
- # if there exists a similar moving piece on the same rank and file as
- # the move in question, use the square as the disambiguator
- if same_rank > 0 && same_file > 0:
- return algebraic(from)
- elif same_file > 0:
- # if the moving piece rests on the same file, use the rank symbol as the
- # disambiguator
- return algebraic(from)[1]
- else:
- # else use the file symbol
- return algebraic(from)[0]
- return ""
-
-
-static func infer_piece_type(san: String) -> String:
- var piece_type := san[0]
- if piece_type >= "a" && piece_type <= "h":
- return PAWN
- piece_type = piece_type.to_lower()
- if piece_type == "o":
- return KING
- return piece_type
-
-
-# WARNING: If `localized` is disabled, you must use localize_piece_move on the returned piece object.
-func piece_moves(_square_: String, piece: String, color := turn, localize := true) -> Array:
- var moves := []
- var second_rank := {b = RANK_7, w = RANK_2}
- var sq: int = SQUARE_MAP[_square_]
- if piece == PAWN:
- # single square, non capturing
- var square = sq + PAWN_OFFSETS[color][0]
- if square <= SQUARE_MAP.h1:
- __add_piece_move(moves, sq, square, piece)
-
- # double square
- var _square: int = sq + PAWN_OFFSETS[color][1]
- if second_rank[color] == rank(sq):
- __add_piece_move(moves, sq, _square, piece, BITS.BIG_PAWN)
-
- # pawn captures
- for j in range(2, 4):
- var _square: int = sq + PAWN_OFFSETS[color][j]
- if _square & 0x88: # off the board
- continue
- __add_piece_move(moves, sq, _square, piece, BITS.CAPTURE)
- else:
- for offset in PIECE_OFFSETS[piece]:
- var square := sq
- while true:
- square += offset
- if square & 0x88:
- break
-
- __add_piece_move(moves, sq, square, piece)
- if piece in KNIGHT + KING:
- break
-
- if piece == KING and _square_ in ["e1", "e8"]:
- __add_piece_move(moves, sq, sq + 2, piece, BITS.KSIDE_CASTLE)
- __add_piece_move(moves, sq, sq - 2, piece, BITS.QSIDE_CASTLE)
-
- if localize:
- for i in range(len(moves)):
- moves[i] = localize_piece_move(moves[i])
- return moves
-
-
-static func __add_piece_move(moves: Array, from: int, to: int, piece: String, flags := BITS.NORMAL) -> void:
- var mov = {from = from, to = to, flags = flags}
- if piece == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1):
- for p in [QUEEN, ROOK, BISHOP, KNIGHT]:
- var m = mov.duplicate()
- m["promotion"] = p
- m.flags |= BITS.PROMOTION
- moves.append(m)
- else:
- moves.append(mov)
-
-
-# Please only use the returned object if the team the move was created for is the current turn.
-func localize_piece_move(piece_move: Dictionary) -> Dictionary:
- return __build_move(piece_move.from, piece_move.to, piece_move.flags, piece_move.get("promotion", ""))
-
-
-###
-### utility functions
-###
-static func rank(i: int) -> int:
- return i >> 4
-
-
-static func file(i: int) -> int:
- return i & 15
-
-
-static func vecfrom0x88(i: int) -> Vector2:
- return Vector2(file(i), rank(i))
-
-
-static func vec2algebraic(pos: Vector2) -> String:
- var column := "abcdefgh"[pos.x]
- var row := str(round(8 - pos.y))
- return column + row
-
-
-static func algebraic2vec(alg: String) -> Vector2:
- return Vector2("abcdefgh".find(alg[0]), 8 - int(alg[1]))
-
-
-static func algebraic(i: int) -> String:
- var f := file(i)
- var r := rank(i)
- return "abcdefgh"[f] + "87654321"[r]
-
-
-static func __swap_color(c: String) -> String:
- return BLACK if c == WHITE else WHITE
-
-
-static func offset(pos, offset: Vector2) -> String:
- if typeof(pos) == TYPE_STRING: # algbraic
- return vec2algebraic(algebraic2vec(pos) + offset)
- elif typeof(pos) == TYPE_INT: # board pos
- return vec2algebraic(vecfrom0x88(pos) + offset)
- return ""
-
-
-# begin main functions
-
-var board := [] # stores all the pieces
-var kings := {w = EMPTY, b = EMPTY} # stores the square of the kings
-var turn := WHITE # whose turn is it
-var castling := {w = 0, b = 0} # castling abilities
-var ep_square := EMPTY # current en passant square; will be `e3` if you make `e4`
-var half_moves := 0 # halfmove counter
-var fullmoves := 1 # fullmove counter
-var __history := []
-
-
-func _init(fen := DEFAULT_POSITION) -> void:
- load_fen(fen)
- board.resize(128)
-
-
-# removes everything on the board ( 8/8/8/8/8/8/8/8 w - - 0 1 )
-func clear() -> void:
- board.resize(0)
- board.resize(128)
- kings = {w = EMPTY, b = EMPTY}
- turn = WHITE
- castling = {w = 0, b = 0}
- ep_square = EMPTY
- half_moves = 0
- fullmoves = 1
- __history = []
-
-
-# goes back to the default position
-func reset() -> void:
- load_fen(DEFAULT_POSITION)
-
-
-# loads a FEN string. see `fen()`
-# returns false in the event of a failure to parse the FEN string.
-func load_fen(fen):
- var parsed: Dictionary = Fen.parse(fen)
- if !parsed:
- return false
- clear()
-
- for x in range(8):
- for y in range(8):
- var piece: String = parsed.mat[y][x]
- if piece:
- put(
- {type = piece.to_lower(), color = WHITE if piece < "a" else BLACK},
- vec2algebraic(Vector2(x, y))
- )
- turn = parsed.turn
- if "K" in parsed.castling:
- castling.w |= BITS.KSIDE_CASTLE
- if "Q" in parsed.castling:
- castling.w |= BITS.QSIDE_CASTLE
- if "k" in parsed.castling:
- castling.b |= BITS.KSIDE_CASTLE
- if "q" in parsed.castling:
- castling.b |= BITS.QSIDE_CASTLE
-
- ep_square = SQUARE_MAP[parsed.enpassant] if parsed.enpassant != "-" else EMPTY
- half_moves = parsed.halfmove
- fullmoves = parsed.fullmove
- return true
-
-
-# returns the FEN string of the current position
-func fen() -> String:
- var empty := 0
- var pieces := ""
- var i := 0
- while i < SQUARE_MAP.h1 + 1:
- if board[i] == null:
- empty += 1
- else:
- if empty > 0:
- pieces += str(empty)
- empty = 0
- var piece: String = board[i].type
- var color: String = board[i].color
- pieces += piece.to_upper() if color == WHITE else piece.to_lower()
-
- if (i + 1) & 0x88:
- if empty > 0:
- pieces += str(empty)
-
- if i != SQUARE_MAP.h1:
- pieces += "/"
-
- empty = 0
- i += 8
- i += 1
-
- var cflags := ""
- if castling[WHITE] & BITS.KSIDE_CASTLE:
- cflags += "K"
- if castling[WHITE] & BITS.QSIDE_CASTLE:
- cflags += "Q"
- if castling[BLACK] & BITS.KSIDE_CASTLE:
- cflags += "k"
- if castling[BLACK] & BITS.QSIDE_CASTLE:
- cflags += "q"
- cflags = cflags if cflags else "-"
- var epflags := "-" if ep_square == EMPTY else algebraic(ep_square)
-
- return "%s %s %s %s %s %s" % [pieces, turn, cflags, epflags, half_moves, fullmoves]
-
-
-# gets a square from the board
-func get(square: String) -> Dictionary:
- var piece = board[SQUARE_MAP[square]]
- return {type = piece.type, color = piece.color} if piece else {}
-
-
-# PUTs a piece object into the specified square on the board, returns OK on sucess
-func put(piece: Dictionary, square: String) -> int:
- # check for valid piece object + valid piece + valid square
- if (
- !("type" in piece && "color" in piece)
- or SYMBOLS.find(piece.type.to_lower()) == -1
- or not square in SQUARE_MAP
- ):
- return ERR_INVALID_DATA
-
- var sq: int = SQUARE_MAP[square]
-
- # only one king
- if piece.type == KING && !(kings[piece.color] == EMPTY || kings[piece.color] == sq):
- return ERR_ALREADY_EXISTS
-
- board[sq] = {type = piece.type, color = piece.color}
- if piece.type == KING:
- kings[piece.color] = sq
-
- return OK
-
-
-func remove(square) -> Dictionary:
- var piece := get(square)
- if piece:
- board[SQUARE_MAP[square]] = null
- if piece && piece.type == KING:
- kings[piece.color] = EMPTY
- return piece
-
-
-func __add_move(moves: Array, from: int, to: int, flags := BITS.NORMAL, b := board) -> void:
- # if pawn promotion
- if b[from].type == PAWN && (rank(to) == RANK_8 || rank(to) == RANK_1):
- for p in [QUEEN, ROOK, BISHOP, KNIGHT]:
- moves.append(__build_move(from, to, flags, p))
- else:
- moves.append(__build_move(from, to, flags))
-
-
-func __build_move(from: int, to: int, flags: int = BITS.NORMAL, promotion := "", _board: Array = board):
- var move := {
- color = turn,
- from = from,
- to = to,
- flags = flags,
- piece = _board[from].type,
- }
-
- if promotion:
- move.flags |= BITS.PROMOTION
- move.promotion = promotion
-
- if _board[to]:
- move.captured = _board[to].type
- elif flags & BITS.EP_CAPTURE:
- move.captured = PAWN
- return move
-
-
-func __generate_moves(options := {}) -> Array:
- var moves := []
- var us := turn
- var them := __swap_color(us)
- var second_rank := {b = RANK_7, w = RANK_2}
-
- var first_sq: int = SQUARE_MAP.a8 - 1
- var last_sq: int = SQUARE_MAP.h1
- var single_square := false
-
- # legal moves?
- var legal: bool = options.legal if "legal" in options else true
- var piece_type: String = (
- options.piece.to_lower()
- if "piece" in options and typeof(options.piece) == TYPE_STRING
- else "-1"
- )
- # generating moves for a single square?
- if "square" in options:
- if options.square in SQUARE_MAP:
- last_sq = SQUARE_MAP[options.square]
- first_sq = last_sq - 1
- single_square = true
- else:
- return []
-
- var i := first_sq
- while i < last_sq:
- i += 1
- # are we off the edge of the board
- if i & 0x88:
- i += 7
- continue
- var piece = board[i]
- if piece == null || piece.color != us:
- continue
-
- if piece.type == PAWN && (piece_type == "-1" || piece_type == PAWN):
- # single square, non capturing
- var square = i + PAWN_OFFSETS[us][0]
- if square <= SQUARE_MAP.h1 and board[square] == null:
- __add_move(moves, i, square, BITS.NORMAL)
-
- # double square
- var _square: int = i + PAWN_OFFSETS[us][1]
- if second_rank[us] == rank(i) && board[_square] == null:
- __add_move(moves, i, _square, BITS.BIG_PAWN)
-
- # pawn captures
- for j in range(2, 4):
- var _square: int = i + PAWN_OFFSETS[us][j]
- if _square & 0x88:
- continue
-
- if board[_square] != null && board[_square].color == them:
- __add_move(moves, i, _square, BITS.CAPTURE)
- elif _square == ep_square:
- __add_move(moves, i, ep_square, BITS.EP_CAPTURE)
- elif piece_type == "-1" || piece_type == piece.type:
- for offset in PIECE_OFFSETS[piece.type]:
- var square := i
-
- while true:
- square += offset
- if square & 0x88:
- break
-
- if board[square] == null:
- __add_move(moves, i, square, BITS.NORMAL)
- else:
- if board[square].color == us:
- break
- __add_move(moves, i, square, BITS.CAPTURE)
- break
-
- # break, if knight or king
- if piece.type == "n" || piece.type == "k":
- break
-
- # check for castling if: a) we're generating all moves, or b) we're doing
- # single square move generation on the king's square
- if piece_type == "-1" || piece_type == KING:
- if !single_square || last_sq == kings[us]:
- # king-side castling
- if castling[us] & BITS.KSIDE_CASTLE:
- var castling_from: int = kings[us]
- var castling_to: int = castling_from + 2
-
- if (
- board[castling_from + 1] == null
- && board[castling_to] == null
- && !__attacked(them, kings[us])
- && !__attacked(them, castling_from + 1)
- && !__attacked(them, castling_to)
- ):
- __add_move(moves, kings[us], castling_to, BITS.KSIDE_CASTLE)
-
- # queen-side castling
- if castling[us] & BITS.QSIDE_CASTLE:
- var castling_from: int = kings[us]
- var castling_to := castling_from - 2
-
- if (
- board[castling_from - 1] == null
- && board[castling_from - 2] == null
- && board[castling_from - 3] == null
- && !__attacked(them, kings[us])
- && !__attacked(them, castling_from - 1)
- && !__attacked(them, castling_to)
- ):
- __add_move(moves, kings[us], castling_to, BITS.QSIDE_CASTLE)
-
- # return all pseudo-legal moves (this includes moves that allow the king
- # to be captured)
- if !legal:
- return moves
-
- # filter out illegal moves
- var legal_moves := []
- for move in moves:
- __make_move(move)
- if !__king_attacked(us):
- legal_moves.append(move)
- __undo_move()
-
- return legal_moves
-
-
-# convert a move from 0x88 coordinates to Standard Algebraic Notation
-# (SAN)
-#
-# @param {boolean} sloppy Use the sloppy SAN generator to work around over
-# disambiguation bugs in Fritz and Chessbase. See below:
-#
-# r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
-# 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
-# 4. ... Ne7 is technically the valid SAN
-func __move_to_san(move, moves := __generate_moves({legal = true}), annotations := true) -> String:
- var output := ""
-
- if move.flags & BITS.KSIDE_CASTLE:
- output = "O-O"
- elif move.flags & BITS.QSIDE_CASTLE:
- output = "O-O-O"
- else:
- if move.piece != PAWN:
- var disambiguator := get_disambiguator(move, moves)
- output += move.piece.to_upper() + disambiguator
-
- if move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE):
- if move.piece == PAWN:
- output += algebraic(move.from)[0]
- output += "x"
-
- output += algebraic(move.to)
-
- if move.flags & BITS.PROMOTION:
- output += "=" + move.promotion.to_upper()
- if annotations:
- __make_move(move)
- if in_check():
- if in_checkmate():
- output += "#"
- else:
- output += "+"
- __undo_move()
- return output
-
-
-func __attacked(color: String, square: int):
- var i := -1
- while i < SQUARE_MAP.h1:
- i += 1
- # did we run off the end of the board
- if i & 0x88:
- i += 7
- continue
-
- # if empty square or wrong color
- if board[i] == null || board[i].color != color:
- continue
-
- var piece: Dictionary = board[i]
- var difference := i - square
- var index := difference + 119
-
- if ATTACKS[index] & (1 << SHIFTS[piece.type]):
- if piece.type == PAWN:
- if difference > 0:
- if piece.color == WHITE:
- return true
- else:
- if piece.color == BLACK:
- return true
- continue
-
- # if the piece is a knight or a king
- if piece.type == "n" || piece.type == "k":
- return true
-
- var offset := RAYS[index]
- var j := i + offset
-
- var blocked := false
- while j != square:
- if board[j] != null:
- blocked = true
- break
- j += offset
-
- if !blocked:
- return true
- return false
-
-
-func __king_attacked(color):
- return __attacked(__swap_color(color), kings[color])
-
-
-func in_check():
- return __king_attacked(turn)
-
-
-func in_checkmate():
- return in_check() && __generate_moves().size() == 0
-
-
-func in_stalemate():
- return !in_check() && __generate_moves().size() == 0
-
-
-func insufficient_material():
- var pieces := {b = 0, k = 0, r = 0, q = 0, n = 0, p = 0}
- var bishops := []
- var num_pieces := 0
- var sq_color := 0
- var i := -1
- while i < SQUARE_MAP.h1:
- i += 1
-
- sq_color = (sq_color + 1) % 2
- if i & 0x88:
- i += 7
- continue
-
- var piece = board[i]
- if piece:
- pieces[piece.type] += 1
- if piece.type == BISHOP:
- bishops.append(sq_color)
- num_pieces += 1
- # k vs k
- if num_pieces == 2:
- return true
- elif num_pieces == 3 && (pieces[BISHOP] == 1 || pieces[KNIGHT] == 1):
- # k vs. kn .... or .... k vs. kb
- return true
- elif num_pieces == pieces[BISHOP] + 2:
- # kb vs. kb where any number of bishops are all on the same color
- var sum := 0
- var lent := bishops.size()
- for b in bishops:
- sum += b
- if sum == 0 || sum == lent: # check if on same color
- return true
- return false
-
-
-func in_threefold_repetition():
- # TODO: while this func is fine for casual use, a better
- # implementation would use a Zobrist key (instead of FEN). the
- # Zobrist key would be maintained in the __make_move/__undo_move funcs,
- # avoiding the costly funcs that we do below.
- var moves := []
- var positions := {}
- var repetition := false
-
- while true:
- var move: Dictionary = __undo_move()
- if !move:
- break
- moves.append(move)
-
- while true:
- # remove the last two fields in the FEN string, they're not needed
- # when checking for draw by rep
- if !repetition:
- var fen := PoolStringArray(Array(fen().split(" ")).slice(0, 3)).join(" ")
-
- # has the position occurred three or more times
- positions[fen] = positions[fen] + 1 if fen in positions else 1
- if positions[fen] >= 3:
- repetition = true
-
- if !moves.size():
- break
- __make_move(moves.pop_back())
-
- return repetition
-
-
-func __push(move):
- __history.append(
- {
- move = move,
- kings = {b = kings.b, w = kings.w},
- turn = turn,
- castling = {b = castling.b, w = castling.w},
- ep_square = ep_square,
- half_moves = half_moves,
- fullmoves = fullmoves,
- }
- )
-
-
-func __make_move(move: Dictionary):
- var us := turn
- var them := __swap_color(us)
- __push(move)
-
- board[move.to] = board[move.from]
- board[move.from] = null
-
- # if ep capture, remove the captured pawn
- if move.flags & BITS.EP_CAPTURE:
- board[move.to + (-16 if us == BLACK else 16)] = null
-
- # if pawn promotion, replace with new piece
- if move.flags & BITS.PROMOTION:
- board[move.to] = {type = move.promotion, color = us}
-
- # if we moved the king
- if board[move.to].type == KING:
- kings[board[move.to].color] = move.to
-
- # if we castled, move the rook next to the king
- if move.flags & BITS.KSIDE_CASTLE:
- var castling_to: int = move.to - 1
- var castling_from: int = move.to + 1
- board[castling_to] = board[castling_from]
- board[castling_from] = null
- elif move.flags & BITS.QSIDE_CASTLE:
- var castling_to: int = move.to + 1
- var castling_from: int = move.to - 2
- board[castling_to] = board[castling_from]
- board[castling_from] = null
-
- # turn off castling
- castling[us] = 0
-
- # turn off castling if we move a rook
- if castling[us]:
- for rook in ROOKS[us]:
- if move.from == rook.square && castling[us] & rook.flag:
- castling[us] ^= rook.flag
- break
- # turn off castling if we capture a rook
- if castling[them]:
- for rook in ROOKS[them]:
- if move.to == rook.square && castling[them] & rook.flag:
- castling[them] ^= rook.flag
- break
-
- # if big pawn move, update the en passant square
- ep_square = (move.to + (-16 if turn == BLACK else 16)) if move.flags & BITS.BIG_PAWN else EMPTY
-
- # reset the 50 move counter if a pawn is moved or a piece is captured
- half_moves = 0 if move.piece == PAWN or move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE) else (half_moves + 1)
-
- fullmoves += 1 if turn == BLACK else 0
- turn = __swap_color(turn)
-
-
-func __undo_move() -> Dictionary:
- var old = __history.pop_back()
- if old == null:
- return {}
-
- var move: Dictionary = old.move
- kings = old.kings
- turn = old.turn
- castling = old.castling
- ep_square = old.ep_square
- half_moves = old.half_moves
- fullmoves = old.fullmoves
-
- var us := turn
- var them := __swap_color(turn)
-
- board[move.from] = board[move.to]
- board[move.from].type = move.piece # to undo any promotions
- board[move.to] = null
- if move.flags & BITS.CAPTURE:
- board[move.to] = {type = move.captured, color = them}
- elif move.flags & BITS.EP_CAPTURE:
- board[move.to + (-16 if us == BLACK else 16)] = {type = PAWN, color = them}
-
- if move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE):
- var castling_to
- var castling_from
- if move.flags & BITS.KSIDE_CASTLE:
- castling_to = move.to + 1
- castling_from = move.to - 1
- elif move.flags & BITS.QSIDE_CASTLE:
- castling_to = move.to - 2
- castling_from = move.to + 1
-
- board[castling_to] = board[castling_from]
- board[castling_from] = null
-
- return move
-
-
-# convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
-func __move_from_san(move, sloppy := false) -> Dictionary:
- # strip off any move decorations: e.g Nf3+?! becomes Nf3
- var clean_move := stripped_san(move)
-
- var overly_disambiguated := false
- var piece
- var from
- var to
- var promotion
- var matches
-
- # the move parsers is a 2-step state
- for parser in range(2):
- if parser == PARSER_SLOPPY:
- # only run the sloppy parse if explicitly requested
- if !sloppy:
- return {}
-
- # The sloppy parser allows the user to parse non-standard chess
- # notations. This parser is opt-in (by specifying the
- # '{ sloppy: true }' setting) and is only run after the Standard
- # Algebraic Notation (SAN) parser has failed.
- #
- # When running the sloppy parser, we'll run a regex to grab the piece,
- # the to/from square, and an optional promotion piece. This regex will
- # parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7,
- # f7f8q, b1c3
- #
- # NOTE: Some positions and moves may be ambiguous when using the
- # sloppy parser. For example, in this position:
- # 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, the move b1c3 may be interpreted
- # as Nc3 or B1c3 (a disambiguated bishop move). In these cases, the
- # sloppy parser will default to the most most basic interpretation
- # (which is b1c3 parsing to Nc3).
-
- var move_regex := RegEx.new()
- move_regex.compile("([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])")
-
- # The [a-h]?[1-8]? portion of the regex below handles moves that may
- # be overly disambiguated (e.g. Nge7 is unnecessary and non-standard
- # when there is one legal knight move to e7). In this case, the value
- # of 'from' variable will be a rank or file, not a square.
- var fallback := RegEx.new()
- fallback.compile("([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?")
-
- var result := move_regex.search(clean_move)
- if result:
- matches = result.strings
- piece = matches[1]
- from = matches[2]
- to = matches[3]
- promotion = matches[4]
- else:
- var _result := fallback.search(clean_move)
-
- if _result:
- matches = _result.strings
- piece = matches[1]
- from = matches[2]
- to = matches[3]
- promotion = matches[4]
-
- if from and from.length() == 1:
- overly_disambiguated = true
-
- var piece_type := infer_piece_type(clean_move)
- var moves := __generate_moves(
- {
- legal = true,
- piece = piece if piece else piece_type,
- }
- )
- for move in moves:
- match parser:
- PARSER_STRICT:
- var m := stripped_san(__move_to_san(move, moves, false))
- if clean_move == m:
- return move
- continue
- PARSER_SLOPPY:
- if matches:
- # hand-compare move properties with the results from our sloppy regex
- if (
- (!piece || piece.to_lower() == move.piece)
- && from in SQUARE_MAP
- && to in SQUARE_MAP
- && SQUARE_MAP[from] == move.from
- && SQUARE_MAP[to] == move.to
- && (!promotion || promotion.to_lower() == move.promotion)
- ):
- return move
- elif overly_disambiguated:
- # SPECIAL CASE: we parsed a move string that may have an
- # unneeded rank/file disambiguator (e.g. Nge7). The 'from'
- # variable will be validated
- var square := algebraic(move.from)
- if (
- (!piece || piece.to_lower() == move.piece)
- && to in SQUARE_MAP
- && SQUARE_MAP[to] == move.to
- && (from == square[0] || from == square[1])
- && (!promotion || promotion.to_lower() == move.promotion)
- ):
- return move
- return {}
-
-
-func __make_pretty(ugly_move: Dictionary) -> Dictionary:
- var move := ugly_move.duplicate()
- move.san = __move_to_san(move, __generate_moves({legal = true}))
- move.to = algebraic(move.to)
- move.from = algebraic(move.from)
-
- var flags := ""
-
- for flag in BITS:
- if BITS[flag] & move.flags:
- flags += FLAGS[flag]
-
- move.flags = flags
- return move
-
-
-#*****************************************************************************
-#* DEBUGGING UTILITIES
-#****************************************************************************/
-func perft(depth: int) -> int:
- var moves := __generate_moves({legal = false})
- var nodes := 0
- var color := turn
- for move in moves:
- __make_move(move)
- if !__king_attacked(color):
- if depth - 1 > 0:
- var child_nodes := perft(depth - 1)
- nodes += child_nodes
- else:
- nodes += 1
- __undo_move()
-
- return nodes
-
-
-#***************************************************************************
-#* PUBLIC API
-#***************************************************************************
-
-
-func in_draw():
- return half_moves >= 50 || in_stalemate() || insufficient_material() || in_threefold_repetition()
-
-
-func game_over():
- return (
- half_moves >= 50
- || in_checkmate()
- || in_stalemate()
- || insufficient_material()
- || in_threefold_repetition()
- )
-
-
-# lists the possible legal moves.
-func moves(options := {}):
- # The internal representation of a chess move is in 0x88 format, and
- # not meant to be human-readable. The code below converts the 0x88
- # square coordinates to algebraic coordinates. It also prunes an
- # unnecessary move keys resulting from a verbose call.
-
- var ugly_moves := __generate_moves(options)
- var moves := []
- for ugly_move in ugly_moves:
- # does the user want a full move object (most likely not), or just
- # SAN
- if "verbose" in options && options.verbose:
- moves.append(__make_pretty(ugly_move))
- elif "stripped" in options && options.stripped:
- moves.append(stripped_san(__move_to_san(ugly_move, __generate_moves({legal = true}), false)))
- else:
- moves.append(__move_to_san(ugly_move, __generate_moves({legal = true})))
- return moves
-
-
-# warning-ignore:function_conflicts_variable
-# returns a 2d matrix of the board.
-func board():
- var output := []
- var row := []
- var i := -1
- while i < SQUARE_MAP.h1:
- i += 1
- if board[i] == null:
- row.append(null)
- else:
- row.append(
- {
- square = algebraic(i),
- type = board[i].type,
- color = board[i].color,
- }
- )
- if (i + 1) & 0x88:
- output.append(row)
- row = []
- i += 8
- return output
-
-
-func pgn() -> String:
- # using the specification from http://www.chessclub.com/help/PGN-spec
- # pop all of __history onto reversed_history
- var reversed_history := []
- while !__history.empty():
- reversed_history.append(__undo_move())
- var moves: PoolStringArray = []
- var move_string := ""
- # build the list of moves. a move_string looks like: "3. e3 e6"
- while reversed_history.size() > 0:
- var move: Dictionary = reversed_history.pop_back()
-
- # if the position started with black to move, start PGN with 1. ...
- if !__history.size() and move.color == "b":
- move_string = "%s ..." % fullmoves
- elif move.color == "w":
- if move_string.length() > 0:
- moves.append(move_string)
- move_string = "%s." % fullmoves
-
- move_string += " %s" % __move_to_san(move, __generate_moves({legal = true}))
- __make_move(move)
-
- if move_string.length():
- moves.append(move_string)
-
- # __history should be back to what it was before we started generating PGN,
- # so join together moves
- return moves.join(" ")
-
-
-func load_pgn(pgn: String, options := {}) -> int:
- # allow the user to specify the sloppy move parser to work around over
- # disambiguation bugs in Fritz and Chessbase
- var sloppy: bool = options.sloppy if "sloppy" in options else false
-
- var parsed: Dictionary = Pgn.parse(pgn)
-
- # Put the board in the starting position
- reset()
-
- var fen := ""
-
- for key in parsed.headers:
- # check to see user is including fen (possibly with wrong tag case)
- if key.to_lower() == "fen":
- fen = parsed.headers[key]
-
- # sloppy parser should attempt to load a fen tag, even if it's
- # the wrong case and doesn't include a corresponding [SetUp "1"] tag */
- if sloppy and fen:
- if !load_fen(fen):
- return ERR_INVALID_DATA
- else:
- # strict parser - load_fen the starting position indicated by [Setup '1']
- # and [FEN position]
- if "SetUp" in parsed.headers and parsed.headers["SetUp"] == "1":
- if !("FEN" in parsed.headers && load_fen(parsed.headers["FEN"])):
- return ERR_INVALID_DATA
-
- var moves: Array = parsed.moves
- var move := {}
- var result := ""
- for mov in moves:
- move = __move_from_san(mov, sloppy)
- if !move:
- # was the move an end of game marker
- if TERMINATION_MARKERS.find(mov) != -1:
- result = mov
- else:
- return ERR_INVALID_DATA
- else:
- # reset the end of game marker if making a valid move
- result = ""
- __make_move(move)
- # Per section 8.2.6 of the PGN spec, the Result tag pair must match
- # match the termination marker. Only do this when headers are present,
- # but the result tag is missing
- if result && parsed.headers.size() && !parsed.header["Result"]:
- moves.append(result)
- return OK
-
-
-# The move func can be called with in the following parameters:
-#
-# .move('Nxb7') <- where 'move' is a case-sensitive SAN string
-#
-# .move({ from: 'h7', to :'h8', promotion: 'q'}) <- where the 'move' is a move obj
-func move(move, sloppy := false) -> Dictionary:
- var move_obj = null
-
- if typeof(move) == TYPE_STRING:
- move_obj = __move_from_san(move, sloppy)
- elif typeof(move) == TYPE_DICTIONARY:
- var moves := __generate_moves()
-
- # uglify move
- for m in moves:
- if (
- move.from == algebraic(m.from)
- && move.to == algebraic(m.to)
- && (!("promotion" in m) || move.promotion == m.promotion)
- ):
- move_obj = m
- break
-
- # failed to find move
- if !move_obj:
- return {}
-
- # need to make a copy of move because we can't generate SAN after the
- # move is made
- var pretty_move := __make_pretty(move_obj)
-
- __make_move(move_obj)
-
- return pretty_move
-
-
-func undo() -> Dictionary:
- var move := __undo_move()
- return __make_pretty(move) if move else {}
-
-
-func ascii() -> String:
- var s := " +------------------------+\n"
- var i := 0
- while i < SQUARE_MAP.h1 + 1:
- # display the rank
- if file(i) == 0:
- s += " " + "87654321"[rank(i)] + " |"
-
- # empty piece
- if board[i] == null:
- s += " . "
- else:
- var piece: String = board[i].type
- var color: String = board[i].color
- var symbol := piece.to_upper() if color == WHITE else piece.to_lower()
- s += " " + symbol + " "
- if (i + 1) & 0x88:
- s += "|\n"
- i += 8
- i += 1
- s += " +------------------------+\n"
- s += " a b c d e f g h"
-
- return s
-
-
-static func square_color(square):
- if square in SQUARE_MAP:
- var sq_0x88: int = SQUARE_MAP[square]
- return "light" if (rank(sq_0x88) + file(sq_0x88)) % 2 == 0 else "dark"
- return null
-
-
-func history(verbose := false) -> Array:
- var reversed_history := []
- var move_history := []
-
- while __history.size() > 0:
- reversed_history.append(__undo_move())
-
- while reversed_history.size() > 0:
- var move: Dictionary = reversed_history.pop_back()
- if verbose:
- move_history.append(__make_pretty(move))
- else:
- move_history.append(__move_to_san(move, __generate_moves({legal = true})))
- __make_move(move)
-
- return move_history
diff --git a/export_presets.cfg b/export_presets.cfg
index f5393eb..4fd2284 100644
--- a/export_presets.cfg
+++ b/export_presets.cfg
@@ -79,7 +79,7 @@ platform="HTML5"
runnable=true
custom_features=""
export_filter="all_resources"
-include_filter="COPYING.md, LICENSE, version"
+include_filter="COPYING.md, LICENSE, version, *.js"
exclude_filter="assets/pieces/alpha/*, assets/pieces/governor/*, assets/pieces/horsey/*, assets/pieces/libra/*, assets/pieces/maestro/*, assets/pieces/pixel/*"
export_path="exports/folder/index.html"
script_export_mode=1
@@ -94,11 +94,11 @@ vram_texture_compression/for_desktop=false
vram_texture_compression/for_mobile=false
html/export_icon=true
html/custom_html_shell="res://html/custom.html"
-html/head_include=""
+html/head_include="<script src=\"lib/stockfish.js\"></script>"
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=true
-progressive_web_app/enabled=true
+progressive_web_app/enabled=false
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
diff --git a/godot.lock b/godot.lock
index 51fd93a..3d0d1f6 100644
--- a/godot.lock
+++ b/godot.lock
@@ -2,5 +2,9 @@
"@bendn/gdcli": {
"version": "1.2.5",
"integrity": "sha512-/YOAd1+K4JlKvPTmpX8B7VWxGtFrxKq4R0A6u5qOaaVPK6uGsl4dGZaIHpxuqcurEcwPEOabkoShXKZaOXB0lw=="
+ },
+ "@bendn/stockfish.gd": {
+ "version": "2.0.0",
+ "integrity": "sha512-ks0zjROWWATbU1FpKlp3Lm9Cxz7GWNnRWhiE1+Ogks+CD7UX5GL+E85mVxp8dv+gsB3mcn2ZSrbn59XQT1IcvA=="
}
} \ No newline at end of file
diff --git a/godot.package b/godot.package
index d6fc57c..5b64564 100644
--- a/godot.package
+++ b/godot.package
@@ -1,6 +1,7 @@
{
"name": "chess",
"packages": {
- "@bendn/gdcli": "1.2.5"
+ "@bendn/gdcli": "1.2.5",
+ "@bendn/stockfish.gd": "2.0.0"
}
} \ No newline at end of file
diff --git a/networking/PacketHandler.gd b/networking/PacketHandler.gd
index 5a950e3..2a55dd7 100644
--- a/networking/PacketHandler.gd
+++ b/networking/PacketHandler.gd
@@ -210,9 +210,8 @@ func _start_game() -> void:
lobby.set_buttons(false)
SoundFx.play("Victory")
- if Globals.team == Chess.BLACK:
- yield(get_tree(), "idle_frame")
- b.flip_board()
+ yield(get_tree(), "idle_frame")
+ b.auto_flip()
func rejoin(tries := 5, interval := 2) -> int: # on disconnect, try to rejoin
diff --git a/project.godot b/project.godot
index 8133b1e..21a425c 100644
--- a/project.godot
+++ b/project.godot
@@ -39,10 +39,10 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://ui/checkboxbutton/CheckBoxButton.gd"
}, {
-"base": "Resource",
+"base": "Reference",
"class": "Chess",
"language": "GDScript",
-"path": "res://board/chess.gd"
+"path": "res://addons/stockfish.gd/chess.gd"
}, {
"base": "Control",
"class": "ColorPickerBetter",
@@ -84,10 +84,10 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://ui/chat/ExpandableTextEdit.gd"
}, {
-"base": "Node",
+"base": "Reference",
"class": "FEN",
"language": "GDScript",
-"path": "res://FEN/Fen.gd"
+"path": "res://addons/stockfish.gd/fen.gd"
}, {
"base": "Button",
"class": "FlipButton",
@@ -95,6 +95,11 @@ _global_script_classes=[ {
"path": "res://ui/menus/sidebarright/flipbutton.gd"
}, {
"base": "Control",
+"class": "GameConfig",
+"language": "GDScript",
+"path": "res://ui/menus/lobby/GameConfig.gd"
+}, {
+"base": "Control",
"class": "GameUI",
"language": "GDScript",
"path": "res://ui/board/Game.gd"
@@ -124,6 +129,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://ui/menus/lobby/Lobby.gd"
}, {
+"base": "Node",
+"class": "Log",
+"language": "GDScript",
+"path": "res://Log.gd"
+}, {
"base": "HBoxContainer",
"class": "MaterialLabel",
"language": "GDScript",
@@ -154,10 +164,10 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://ui/menus/sidebarright/OpeningLabel.gd"
}, {
-"base": "Node",
+"base": "Reference",
"class": "PGN",
"language": "GDScript",
-"path": "res://PGN/PGN.gd"
+"path": "res://addons/stockfish.gd/pgn.gd"
}, {
"base": "Reference",
"class": "Parser",
@@ -209,6 +219,16 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://ui/Status.gd"
}, {
+"base": "Reference",
+"class": "Stockfish",
+"language": "GDScript",
+"path": "res://addons/stockfish.gd/stockfish_wrapper.gd"
+}, {
+"base": "Reference",
+"class": "StockfishLoader",
+"language": "GDScript",
+"path": "res://addons/stockfish.gd/stockfish_loader.gd"
+}, {
"base": "Button",
"class": "TestButton",
"language": "GDScript",
@@ -257,12 +277,14 @@ _global_script_class_icons={
"ExpandableTextEdit": "",
"FEN": "",
"FlipButton": "",
+"GameConfig": "",
"GameUI": "",
"Grid": "",
"GridMenu": "",
"GridMenuButton": "",
"HueSlider": "",
"Lobby": "",
+"Log": "",
"MaterialLabel": "",
"MaterialLabelManager": "",
"MessageList": "",
@@ -280,6 +302,8 @@ _global_script_class_icons={
"SaveLoader": "",
"SliderButton": "",
"StatusLabel": "",
+"Stockfish": "",
+"StockfishLoader": "",
"TestButton": "",
"TextEditor": "",
"UndoButton": "",
@@ -291,7 +315,9 @@ _global_script_class_icons={
[application]
config/name="chess"
-config/description="pog"
+config/description="Chess multiplayer client.
+
+Includes stockfish cross-platform support."
run/main_scene="res://ui/menus/startmenu/StartMenu.tscn"
config/use_custom_user_dir=true
config/custom_user_dir_name="chess"
@@ -310,9 +336,6 @@ SaveLoad="*res://saveload.gd"
ColorBack="*res://ui/background/ColorfullBackground.tscn"
PacketHandler="*res://networking/PacketHandler.gd"
Debug="*res://Debug.gd"
-Pgn="*res://PGN/PGN.gd"
-Log="*res://Log.gd"
-Fen="*res://FEN/Fen.gd"
Creds="*res://Credentials.gd"
[debug]
@@ -328,10 +351,6 @@ window/size/height=800
window/stretch/mode="2d"
window/stretch/aspect="keep"
-[editor]
-
-main_run_args="--join __tests__"
-
[editor_plugins]
enabled=PoolStringArray( )
diff --git a/ui/board/Board.gd b/ui/board/Board.gd
index 0ff3646..e6af0d5 100644
--- a/ui/board/Board.gd
+++ b/ui/board/Board.gd
@@ -55,6 +55,7 @@ var chess := Chess.new()
var local := false
var spectating := false
var team: String
+var auto_change_team := false
func _init():
@@ -99,9 +100,6 @@ func set_take_move_circle_color(
func _ready():
- if !team:
- team = "w"
- local = true
set_take_move_circle_color()
_resized()
Events.connect("turn_over", self, "_on_turn_over")
@@ -109,6 +107,10 @@ func _ready():
create_pieces()
create_squares()
create_labels()
+ yield(get_tree(), "idle_frame")
+ if !team:
+ team = chess.turn
+ auto_change_team = true
Log.debug("board: ready")
@@ -308,8 +310,9 @@ func move(san: String, send := true, create_promotion_input := true) -> void:
var move_0x88 = chess.__move_from_san(san, true)
var valid_moves = chess.moves({square = chess.algebraic(move_0x88.from), stripped = true})
if valid_moves.find(chess.stripped_san(san)) == -1:
- Log.err("Invalid move")
+ Log.err("Invalid move " + san)
return
+ Log.debug("Making move " + san)
chess.__make_move(move_0x88)
if move_0x88.flags & Chess.BITS.CAPTURE:
board[move_0x88.to].took()
@@ -371,7 +374,8 @@ func load_pgn(pgn: String) -> void:
clear_pieces()
create_pieces()
emit_signal("clear_pgn")
- var movs: PoolStringArray = Pgn.parse(pgn).moves
+ var pgn_parser := PGN.new()
+ var movs: PoolStringArray = pgn_parser.parse(pgn).moves
emit_signal("load_pgn", movs)
Log.info("load pgn " + pgn)
Events.emit_signal("turn_over")
@@ -389,10 +393,17 @@ func undo(two: bool = false) -> void:
Events.emit_signal("turn_over")
+func auto_flip():
+ if team == Chess.WHITE and flipped:
+ flip_board()
+ elif team == Chess.BLACK and not flipped:
+ flip_board()
+
+
func _on_turn_over():
- if local:
+ if auto_change_team:
team = chess.turn
- flip_board()
+ auto_flip()
if is_my_turn():
set_take_move_circle_color()
diff --git a/ui/board/Game.gd b/ui/board/Game.gd
index 457a2d5..367c044 100644
--- a/ui/board/Game.gd
+++ b/ui/board/Game.gd
@@ -13,7 +13,7 @@ onready var panels := [
func _ready() -> void:
PacketHandler.connect("info_recieved", self, "_spectate_info" if Globals.spectating else "_on_info")
Events.connect("game_over", self, "_game_over")
- if Globals.grid.local:
+ if Globals.local:
get_tree().call_group("freeinlocalmultiplayer", "queue_free")
@@ -47,5 +47,5 @@ func set_panel(pnl: UserPanel, name: String, country: String) -> void:
func _unhandled_input(event: InputEvent):
- if event is InputEventKey and event.pressed and event.scancode == KEY_Z:
+ if event is InputEventKey and event.pressed and event.scancode == KEY_Z and not Globals.local:
chat.visible = !chat.visible
diff --git a/ui/chat/Chat.gd b/ui/chat/Chat.gd
index ddb3a07..871e6b1 100644
--- a/ui/chat/Chat.gd
+++ b/ui/chat/Chat.gd
@@ -12,8 +12,7 @@ var regexes := [
[Utils.compile("~~([^~]+)~~"), "[s]$1[/s]"],
[Utils.compile("#([^#]+)#"), "[rainbow freq=.3 sat=.7]$1[/rainbow]"],
[Utils.compile("%([^%]+)%"), "[shake rate=20 level=25]$1[/shake]"],
- [Utils.compile("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"), "[url=$2]$1[/url]"], # [foo](bar)
- [Utils.compile("[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"),"[url]$0[/url]"],
+ [Utils.compile("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"), "[url=$2]$1[/url]"], # [foo](bar)
]
@@ -43,12 +42,12 @@ func add_label_with(data: Dictionary) -> void:
func send(t: String) -> void:
t = md2bb(t)
- var name = Creds.get("name") if Creds.get("name") else "Anonymous"
- name += "(%s)" % ("Spectator" if Globals.spectating else Globals.grid.team)
if PacketHandler.is_open_connection():
+ var name = Creds.get("name") if Creds.get("name") else "Anonymous"
+ name += "(%s)" % ("Spectator" if Globals.spectating else Globals.grid.team)
PacketHandler.relay_signal({"text": t, "who": name}, PacketHandler.RELAYHEADERS.chat)
else:
- add_label_with({text = t, who = name}) # for testing
+ add_label_with({text = t, who = Creds.get("name")}) # for testing
# markdown to bbcode
@@ -58,8 +57,6 @@ func md2bb(input: String) -> String:
if result:
var index = input.find(result.strings[0]) - 1
var char_before = input[index]
- if replacement[1] == "[url]$0[/url]" and char_before == "[":
- continue
if not char_before in "\\": # taboo characters go here
input = replacement[0].sub(input, replacement[1], true)
input = input.replace("\\", "") # remove escapers
diff --git a/ui/menus/LocalMultiplayer.gd b/ui/menus/LocalMultiplayer.gd
deleted file mode 100644
index e124ca1..0000000
--- a/ui/menus/LocalMultiplayer.gd
+++ /dev/null
@@ -1,43 +0,0 @@
-extends Control
-
-onready var gameconfig = $"%GameConfig"
-
-var in_game := false
-
-
-func _ready():
- gameconfig.connect("done", self, "create")
-
-
-func create(moves: PoolStringArray) -> void:
- var ui: Control = load("res://ui/board/Game.tscn").instance()
- var b: Grid = ui.get_board()
- Log.debug("Set board team to %s" % Utils.expand_color(b.team))
- get_tree().get_root().add_child(ui)
- PacketHandler.lobby.toggle(false)
- yield(get_tree(), "idle_frame")
- b.load_pgn(moves.join(" "))
- Globals.chat.hide()
- in_game = true
- b.team = b.chess.turn
- if b.flipped and b.team == Chess.WHITE:
- b.flip_board()
- get_tree().call_group("userpanel", "hide_children")
- get_tree().call_group("backbutton", "queue_free")
-
-
-func _pressed():
- if gameconfig.visible:
- create(gameconfig.moves)
- else:
- gameconfig.show()
-
-
-func _input(_event):
- if Input.is_action_pressed("ui_cancel") and in_game:
- in_game = false
- PacketHandler.go_back("", true)
- get_node("/root/Game").queue_free()
- PacketHandler.lobby.toggle(true)
- Globals.reset_vars()
- get_parent().current_tab = get_parent().get_children().find(self)
diff --git a/ui/menus/lobby/GameConfig.gd b/ui/menus/lobby/GameConfig.gd
index f1ab473..19dd770 100644
--- a/ui/menus/lobby/GameConfig.gd
+++ b/ui/menus/lobby/GameConfig.gd
@@ -1,19 +1,15 @@
-extends TabContainer
+extends Control
+class_name GameConfig
var moves := PoolStringArray()
var color := true
-export(bool) var color_config := true
-
signal back
-signal done(color, moves)
export(ButtonGroup) var button_group: ButtonGroup
func _ready():
- if not color_config:
- $"".queue_free()
button_group.connect("pressed", self, "_button_pressed")
@@ -21,14 +17,6 @@ func _button_pressed(button: BarTextureButton) -> void:
color = button.name == "White"
-func _on_Continue_pressed():
- if color_config:
- emit_signal("done", color, moves)
- else:
- emit_signal("done", moves)
- reset()
-
-
func _on_Stop_pressed():
emit_signal("back")
reset()
diff --git a/ui/menus/lobby/GameConfig.tscn b/ui/menus/lobby/GameConfig.tscn
index 20090f1..b4701ab 100644
--- a/ui/menus/lobby/GameConfig.tscn
+++ b/ui/menus/lobby/GameConfig.tscn
@@ -1,37 +1,54 @@
-[gd_scene load_steps=8 format=2]
+[gd_scene load_steps=9 format=2]
-[ext_resource path="res://ui/theme/main.theme" type="Theme" id=1]
+[ext_resource path="res://ui/menus/sidebarright/buttonbar.theme" type="Theme" id=1]
[ext_resource path="res://ui/menus/lobby/color.tres" type="ButtonGroup" id=2]
[ext_resource path="res://assets/pieces/cburnett/wK.png" type="Texture" id=3]
[ext_resource path="res://assets/pieces/cburnett/bK.png" type="Texture" id=4]
[ext_resource path="res://ui/menus/lobby/GameConfig.gd" type="Script" id=5]
[ext_resource path="res://ui/menus/lobby/PGNEntry.gd" type="Script" id=6]
[ext_resource path="res://ui/barbutton/BarTextureButton.tscn" type="PackedScene" id=7]
+[ext_resource path="res://ui/theme/main.theme" type="Theme" id=8]
-[node name="GameConfig" type="TabContainer"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-theme = ExtResource( 1 )
+[node name="GameConfig" type="PanelContainer"]
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+margin_left = -210.0
+margin_top = -93.0
+margin_right = 210.0
+margin_bottom = 93.0
+size_flags_horizontal = 4
+size_flags_vertical = 4
+theme = ExtResource( 8 )
script = ExtResource( 5 )
button_group = ExtResource( 2 )
-[node name="" type="VBoxContainer" parent="."]
-anchor_right = 1.0
-anchor_bottom = 1.0
-margin_left = 25.0
-margin_top = 79.0
-margin_right = -25.0
-margin_bottom = -25.0
+[node name="V" type="VBoxContainer" parent="."]
+margin_left = 10.0
+margin_top = 10.0
+margin_right = 410.0
+margin_bottom = 176.0
-[node name="H" type="HBoxContainer" parent=""]
-margin_right = 1372.0
+[node name="Colors" type="HBoxContainer" parent="V"]
+unique_name_in_owner = true
+margin_right = 400.0
margin_bottom = 100.0
+mouse_filter = 2
custom_constants/separation = 0
-alignment = 1
-[node name="Black" parent="/H" instance=ExtResource( 7 )]
-margin_left = 586.0
-margin_right = 686.0
+[node name="ColorLabel" type="Label" parent="V/Colors"]
+margin_top = 33.0
+margin_right = 100.0
+margin_bottom = 67.0
+rect_min_size = Vector2( 100, 0 )
+text = "Color: "
+align = 1
+valign = 1
+
+[node name="Black" parent="V/Colors" instance=ExtResource( 7 )]
+margin_left = 100.0
+margin_right = 200.0
margin_bottom = 100.0
rect_min_size = Vector2( 100, 100 )
toggle_mode = true
@@ -39,9 +56,9 @@ group = ExtResource( 2 )
texture_normal = ExtResource( 4 )
pressed_color = Color( 0.576471, 0.631373, 0.631373, 1 )
-[node name="White" parent="/H" instance=ExtResource( 7 )]
-margin_left = 686.0
-margin_right = 786.0
+[node name="White" parent="V/Colors" instance=ExtResource( 7 )]
+margin_left = 200.0
+margin_right = 300.0
margin_bottom = 100.0
rect_min_size = Vector2( 100, 100 )
toggle_mode = true
@@ -50,59 +67,42 @@ group = ExtResource( 2 )
texture_normal = ExtResource( 3 )
pressed_color = Color( 0.576471, 0.631373, 0.631373, 1 )
-[node name="H2" type="HBoxContainer" parent=""]
+[node name="Stop" type="Button" parent="V/Colors"]
+margin_left = 360.0
+margin_right = 400.0
+margin_bottom = 46.0
+size_flags_horizontal = 10
+size_flags_vertical = 0
+theme = ExtResource( 1 )
+text = "窱"
+
+[node name="PGNInput" type="HBoxContainer" parent="V"]
margin_top = 110.0
-margin_right = 1372.0
+margin_right = 400.0
margin_bottom = 164.0
-alignment = 1
-
-[node name="Stop" type="Button" parent="/H2"]
-margin_left = 535.0
-margin_right = 642.0
-margin_bottom = 54.0
-size_flags_horizontal = 4
-text = "exit"
-
-[node name="Continue" type="Button" parent="/H2"]
-margin_left = 652.0
-margin_right = 836.0
-margin_bottom = 54.0
-size_flags_horizontal = 4
-text = "continue "
-
-[node name="" type="VBoxContainer" parent="."]
-visible = false
-anchor_right = 1.0
-anchor_bottom = 1.0
-margin_left = 30.0
-margin_top = 56.0
-margin_right = -30.0
-margin_bottom = -30.0
-
-[node name="Pgn" type="HBoxContainer" parent=""]
-margin_right = 1362.0
-margin_bottom = 54.0
+rect_min_size = Vector2( 400, 0 )
+size_flags_horizontal = 3
-[node name="Label" type="Label" parent="/Pgn"]
-margin_right = 40.0
-margin_bottom = 34.0
+[node name="Label" type="Label" parent="V/PGNInput"]
+margin_top = 10.0
+margin_right = 68.0
+margin_bottom = 44.0
text = "pgn: "
-[node name="PgnInput" type="LineEdit" parent="/Pgn"]
+[node name="PgnInput" type="LineEdit" parent="V/PGNInput"]
unique_name_in_owner = true
-margin_right = 1352.0
+margin_left = 78.0
+margin_right = 400.0
margin_bottom = 54.0
size_flags_horizontal = 3
placeholder_text = "1. e4"
script = ExtResource( 6 )
-[node name="Checkmark" type="Label" parent="/Pgn"]
-margin_left = 1362.0
+[node name="Checkmark" type="Label" parent="V/PGNInput"]
+visible = false
+margin_left = 500.0
margin_top = 10.0
-margin_right = 1362.0
+margin_right = 500.0
margin_bottom = 44.0
-[connection signal="pressed" from="/H2/Stop" to="." method="_on_Stop_pressed"]
-[connection signal="pressed" from="/H2/Continue" to="." method="_on_Continue_pressed"]
-[connection signal="pgn_selected" from="/Pgn/PgnInput" to="." method="_on_pgn_selected"]
-[connection signal="text_changed" from="/Pgn/PgnInput" to="/Pgn/PgnInput" method="text_changed"]
+[connection signal="pressed" from="V/Colors/Stop" to="." method="_on_Stop_pressed"]
diff --git a/ui/menus/lobby/Lobby.gd b/ui/menus/lobby/Lobby.gd
index ebda154..5540539 100644
--- a/ui/menus/lobby/Lobby.gd
+++ b/ui/menus/lobby/Lobby.gd
@@ -18,7 +18,6 @@ func _ready() -> void:
PacketHandler.connect("hosting", $"%stophost", "set_visible")
PacketHandler.connect("connection_established", self, "reset")
gameconfig.connect("back", self, "reset")
- gameconfig.connect("done", self, "host")
if !Utils.internet:
set_status("no internet", false)
set_buttons(false)
@@ -59,19 +58,24 @@ func _on_join_pressed() -> void:
set_buttons(false)
PacketHandler.join_game()
else:
- set_status("Invalid address", false)
+ set_status("Invalid game code", false)
func _on_HostButton_pressed() -> void:
if gameconfig.visible:
+ if not validate_text():
+ set_status("Invalid game code", false)
+ return
gameconfig.hide()
host(gameconfig.color, gameconfig.moves)
+ set_buttons(false)
return
if validate_text():
- set_buttons(false)
+ for c in buttons.get_children().slice(0, 1):
+ c.disabled = true
gameconfig.show()
else:
- set_status("Invalid address", false)
+ set_status("Invalid game code", false)
func validate_text(text := address.get_text()) -> String:
diff --git a/ui/menus/lobby/Lobby.tscn b/ui/menus/lobby/Lobby.tscn
index 859f8e8..f1d4c0f 100644
--- a/ui/menus/lobby/Lobby.tscn
+++ b/ui/menus/lobby/Lobby.tscn
@@ -21,6 +21,8 @@ margin_bottom = 459.0
[node name="GameConfig" parent="VBox" instance=ExtResource( 5 )]
unique_name_in_owner = true
visible = false
+anchor_left = 0.0
+anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 727.0
diff --git a/ui/menus/lobby/PGNEntry.gd b/ui/menus/lobby/PGNEntry.gd
index 7361911..7be8108 100644
--- a/ui/menus/lobby/PGNEntry.gd
+++ b/ui/menus/lobby/PGNEntry.gd
@@ -5,11 +5,16 @@ onready var checkmark: Label = $"../Checkmark"
signal pgn_selected(m_array)
+func _init() -> void:
+ connect("text_changed", self, "text_changed")
+
+
func text_changed(new_text: String) -> void:
if !new_text:
- checkmark.text = ""
+ checkmark.hide()
return
var status = validate_pgn(new_text)
+ checkmark.show()
if status:
emit_signal("pgn_selected", status)
checkmark.text = ""
@@ -18,7 +23,8 @@ func text_changed(new_text: String) -> void:
func validate_pgn(p: String):
- var parsed = Pgn.parse(p)
+ var pgn_parser := PGN.new()
+ var parsed = pgn_parser.parse(p)
if parsed != null:
var c = Chess.new()
if c.load_pgn(text) == OK and !c.game_over():
diff --git a/ui/menus/local_multiplayer/EngineDepth.gd b/ui/menus/local_multiplayer/EngineDepth.gd
new file mode 100644
index 0000000..bd32c25
--- /dev/null
+++ b/ui/menus/local_multiplayer/EngineDepth.gd
@@ -0,0 +1,21 @@
+extends HBoxContainer
+
+signal depth_changed(new_depth)
+
+var depth: int setget set_depth
+
+onready var depth_label: Label = $"%CurrentDepthLabel"
+
+
+func _ready() -> void:
+ set_depth($"%DepthSlider".value)
+
+
+func set_depth(new_depth: int) -> void:
+ emit_signal("depth_changed", new_depth)
+ depth_label.text = str(new_depth)
+ depth = new_depth
+
+
+func _slid(value: float) -> void:
+ set_depth(value)
diff --git a/ui/menus/local_multiplayer/EngineDepthSlider.tscn b/ui/menus/local_multiplayer/EngineDepthSlider.tscn
new file mode 100644
index 0000000..ff12f58
--- /dev/null
+++ b/ui/menus/local_multiplayer/EngineDepthSlider.tscn
@@ -0,0 +1,43 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://ui/menus/local_multiplayer/EngineDepth.gd" type="Script" id=1]
+[ext_resource path="res://ui/theme/main.theme" type="Theme" id=2]
+
+[node name="EngineDepthSlider" type="HBoxContainer" groups=["freeifnoengine"]]
+margin_top = 238.0
+margin_right = 650.0
+margin_bottom = 272.0
+hint_tooltip = "more depth, more smart, more time spent thinky.
+depth 20: 10-30 seconds."
+mouse_filter = 0
+theme = ExtResource( 2 )
+script = ExtResource( 1 )
+
+[node name="DepthLabel" type="Label" parent="."]
+margin_right = 198.0
+margin_bottom = 34.0
+text = "engine depth:"
+
+[node name="DepthSlider" type="HSlider" parent="."]
+unique_name_in_owner = true
+margin_left = 208.0
+margin_right = 585.0
+margin_bottom = 34.0
+mouse_filter = 1
+size_flags_horizontal = 3
+size_flags_vertical = 3
+min_value = 1.0
+value = 5.0
+rounded = true
+
+[node name="CurrentDepthLabel" type="Label" parent="."]
+unique_name_in_owner = true
+margin_left = 595.0
+margin_right = 650.0
+margin_bottom = 34.0
+rect_min_size = Vector2( 55, 0 )
+text = "1"
+align = 1
+valign = 1
+
+[connection signal="value_changed" from="DepthSlider" to="." method="_slid"]
diff --git a/ui/menus/local_multiplayer/GameConfig.gd b/ui/menus/local_multiplayer/GameConfig.gd
new file mode 100644
index 0000000..f4dbc45
--- /dev/null
+++ b/ui/menus/local_multiplayer/GameConfig.gd
@@ -0,0 +1,27 @@
+extends GameConfig
+
+enum { HUMAN, ENGINE }
+
+var players: PoolIntArray = [
+ HUMAN,
+ HUMAN,
+]
+var depth: int
+
+
+func _player_selected(index: int, player: int) -> void:
+ players[player - 1] = index
+
+
+func _ready() -> void:
+ get_tree().call_group("freeifnoengine", "hide")
+ var loader = StockfishLoader.new()
+ if loader.is_supported():
+ get_tree().call_group("freeifnoengine", "show")
+ return
+ get_tree().call_group("freeifnoengine", "queue_free")
+ get_tree().call_group("showifnoengine", "show")
+
+
+func _depth_changed(new_depth: int) -> void:
+ depth = new_depth
diff --git a/ui/menus/local_multiplayer/GameConfig.tscn b/ui/menus/local_multiplayer/GameConfig.tscn
new file mode 100644
index 0000000..50fd47c
--- /dev/null
+++ b/ui/menus/local_multiplayer/GameConfig.tscn
@@ -0,0 +1,158 @@
+[gd_scene load_steps=13 format=2]
+
+[ext_resource path="res://ui/menus/lobby/GameConfig.tscn" type="PackedScene" id=1]
+[ext_resource path="res://ui/menus/local_multiplayer/PlayerOptionButton.tscn" type="PackedScene" id=2]
+[ext_resource path="res://ui/menus/local_multiplayer/GameConfig.gd" type="Script" id=3]
+[ext_resource path="res://ui/menus/local_multiplayer/EngineDepthSlider.tscn" type="PackedScene" id=4]
+[ext_resource path="res://assets/fonts/ubuntu/ubuntu-normal-nerd.ttf" type="DynamicFontData" id=5]
+[ext_resource path="res://assets/fonts/migu.ttf" type="DynamicFontData" id=6]
+[ext_resource path="res://assets/fonts/ubuntu/ubuntu-bold.ttf" type="DynamicFontData" id=7]
+[ext_resource path="res://ui/menus/local_multiplayer/color.tres" type="ButtonGroup" id=8]
+[ext_resource path="res://ui/menus/local_multiplayer/NoEngineLabel.gd" type="Script" id=9]
+
+[sub_resource type="DynamicFont" id=4]
+size = 15
+font_data = ExtResource( 7 )
+fallback/0 = ExtResource( 5 )
+fallback/1 = ExtResource( 6 )
+
+[sub_resource type="ButtonGroup" id=3]
+
+[sub_resource type="StyleBoxFlat" id=2]
+bg_color = Color( 0.396078, 0.482353, 0.513726, 1 )
+border_width_left = 5
+border_width_top = 5
+border_width_right = 5
+border_width_bottom = 5
+border_color = Color( 0.396078, 0.482353, 0.513726, 1 )
+corner_detail = 1
+
+[node name="GameConfig" instance=ExtResource( 1 )]
+margin_left = -335.0
+margin_top = -124.0
+margin_right = 335.0
+margin_bottom = 124.0
+script = ExtResource( 3 )
+button_group = ExtResource( 8 )
+
+[node name="V" parent="." index="0"]
+margin_right = 660.0
+margin_bottom = 282.0
+
+[node name="Colors" parent="V" index="0"]
+margin_right = 650.0
+
+[node name="Labels" type="VBoxContainer" parent="V/Colors" index="0" groups=["freeifnoengine"]]
+margin_right = 210.0
+margin_bottom = 100.0
+custom_constants/separation = 0
+
+[node name="Label" type="Label" parent="V/Colors/Labels" index="0"]
+margin_right = 210.0
+margin_bottom = 34.0
+rect_min_size = Vector2( 210, 0 )
+text = "Player1 color: "
+align = 1
+valign = 1
+
+[node name="Label2" type="Label" parent="V/Colors/Labels" index="1"]
+margin_top = 34.0
+margin_right = 210.0
+margin_bottom = 51.0
+custom_fonts/font = SubResource( 4 )
+text = "(only matters in p v engine)"
+align = 1
+
+[node name="ColorLabel" parent="V/Colors" index="1"]
+visible = false
+margin_left = 210.0
+margin_right = 310.0
+
+[node name="Black" parent="V/Colors" index="2" groups=["freeifnoengine"]]
+margin_left = 210.0
+margin_right = 310.0
+group = SubResource( 3 )
+
+[node name="White" parent="V/Colors" index="3" groups=["freeifnoengine"]]
+margin_left = 310.0
+margin_right = 410.0
+group = SubResource( 3 )
+
+[node name="Stop" parent="V/Colors" index="4"]
+margin_left = 610.0
+margin_right = 650.0
+
+[node name="PGNInput" parent="V" index="1"]
+margin_right = 650.0
+
+[node name="PgnInput" parent="V/PGNInput" index="1"]
+margin_right = 650.0
+
+[node name="Checkmark" parent="V/PGNInput" index="2"]
+margin_left = 548.0
+margin_right = 548.0
+
+[node name="Players" type="HBoxContainer" parent="V" index="2" groups=["freeifnoengine"]]
+margin_top = 174.0
+margin_right = 650.0
+margin_bottom = 228.0
+
+[node name="1" type="HBoxContainer" parent="V/Players" index="0"]
+margin_right = 335.0
+margin_bottom = 54.0
+
+[node name="Label" type="Label" parent="V/Players/1" index="0"]
+margin_top = 10.0
+margin_right = 115.0
+margin_bottom = 44.0
+text = "player1:"
+
+[node name="PlayerButton" parent="V/Players/1" index="1" instance=ExtResource( 2 )]
+margin_left = 125.0
+margin_right = 335.0
+rect_min_size = Vector2( 210, 0 )
+text = "Human (you)"
+items = [ "Human (you)", null, false, 0, null, "Stockfish", null, false, 1, null ]
+__meta__ = {
+"_editor_description_": ""
+}
+
+[node name="Seperator" type="Panel" parent="V/Players" index="1"]
+margin_left = 345.0
+margin_right = 355.0
+margin_bottom = 54.0
+rect_min_size = Vector2( 10, 0 )
+custom_styles/panel = SubResource( 2 )
+
+[node name="2" type="HBoxContainer" parent="V/Players" index="2"]
+margin_left = 365.0
+margin_right = 650.0
+margin_bottom = 54.0
+
+[node name="Label" type="Label" parent="V/Players/2" index="0"]
+margin_top = 10.0
+margin_right = 115.0
+margin_bottom = 44.0
+text = "player2:"
+
+[node name="PlayerButton" parent="V/Players/2" index="1" instance=ExtResource( 2 )]
+margin_left = 125.0
+margin_right = 285.0
+items = [ "Human", null, false, 0, null, "Stockfish", null, false, 1, null ]
+
+[node name="EngineDepth" parent="V" index="3" instance=ExtResource( 4 )]
+
+[node name="NoEngine" type="RichTextLabel" parent="V" index="4" groups=["showifnoengine"]]
+visible = false
+margin_top = 238.0
+margin_right = 650.0
+margin_bottom = 262.0
+bbcode_enabled = true
+fit_content_height = true
+script = ExtResource( 9 )
+color = Color( 0.709804, 0.537255, 0, 1 )
+
+[connection signal="pgn_selected" from="V/PGNInput/PgnInput" to="." method="_on_pgn_selected"]
+[connection signal="item_selected" from="V/Players/1/PlayerButton" to="." method="_player_selected" binds= [ 1 ]]
+[connection signal="item_selected" from="V/Players/2/PlayerButton" to="." method="_player_selected" binds= [ 2 ]]
+[connection signal="depth_changed" from="V/EngineDepth" to="." method="_depth_changed"]
diff --git a/ui/menus/local_multiplayer/LocalMultiplayer.gd b/ui/menus/local_multiplayer/LocalMultiplayer.gd
new file mode 100644
index 0000000..c7596ec
--- /dev/null
+++ b/ui/menus/local_multiplayer/LocalMultiplayer.gd
@@ -0,0 +1,115 @@
+extends Control
+
+onready var gameconfig := $"%GameConfig"
+
+var in_game := false
+
+enum MODES { PVP, PVE, EVE }
+enum { HUMAN, ENGINE }
+
+var mode: int = -1
+
+var board_engine_bridge: BoardEngineBridge = null
+
+
+func create(moves: PoolStringArray, player1_color: bool, players: PoolIntArray, engine_depth: int) -> void:
+ assign_mode(players)
+ Globals.local = true
+ var ui: Control = load("res://ui/board/Game.tscn").instance()
+ var b: Grid = ui.get_board()
+ b.local = true
+ Log.debug("Set board team to %s" % Utils.expand_color(b.team))
+ get_tree().get_root().add_child(ui)
+ PacketHandler.lobby.toggle(false)
+
+ match mode:
+ MODES.PVP:
+ pass # nothing to do ?
+ MODES.PVE:
+ b.team = "w" if player1_color == true else "b"
+ board_engine_bridge = BoardEngineBridge.new(b, [Chess.__swap_color(b.team)], get_tree(), engine_depth)
+ MODES.EVE:
+ b.team = b.chess.turn
+ Globals.spectating = true
+ board_engine_bridge = BoardEngineBridge.new(b, ["w", "b"], get_tree(), engine_depth)
+ get_tree().call_group("userpanel", "hide_children")
+ get_tree().call_group("backbutton", "queue_free")
+
+ yield(get_tree(), "idle_frame")
+ b.load_pgn(moves.join(" ")) # load_pgn emits Events.turn_over
+ b.auto_flip()
+ Globals.chat.hide()
+
+
+func assign_mode(players: PoolIntArray) -> void:
+ if players.count(HUMAN) == 2:
+ mode = MODES.PVP
+ elif players.count(ENGINE) == 2:
+ mode = MODES.EVE
+ else:
+ mode = MODES.PVE
+
+
+func _pressed():
+ if gameconfig.visible:
+ create(gameconfig.moves, gameconfig.color, gameconfig.players, gameconfig.depth)
+ gameconfig.hide()
+ else:
+ gameconfig.show()
+
+
+func _input(_event):
+ if Input.is_action_pressed("ui_cancel") and Globals.local == true:
+ if board_engine_bridge:
+ board_engine_bridge.kill()
+ board_engine_bridge = null
+ PacketHandler.go_back("", true)
+ get_node("/root/Game").queue_free()
+ PacketHandler.lobby.toggle(true)
+ Globals.reset_vars()
+ get_parent().current_tab = get_parent().get_children().find(self)
+
+
+class BoardEngineBridge:
+ extends Reference
+
+ var b: Grid
+ var stockfish: Stockfish
+ var playing := PoolStringArray()
+ var tree: SceneTree
+ var depth: int
+
+ func _init(board: Grid, teams: PoolStringArray, _tree: SceneTree, _depth: int) -> void:
+ connect_signals()
+ depth = _depth
+ tree = _tree
+ playing = teams
+ b = board
+ var loader = StockfishLoader.new()
+ stockfish = loader.load_stockfish()
+ stockfish.game = b.chess
+
+ func connect_signals():
+ Events.connect("turn_over", self, "turn_over")
+
+ func turn_over():
+ set_engine_position()
+ if stockfish.game.turn in playing:
+ play_bestmove()
+
+ func play_bestmove():
+ yield(tree, "idle_frame")
+ var move: String = yield(bestmove(), "completed")
+ b.move(move, false, false)
+
+ func set_engine_position():
+ stockfish._position()
+
+ func bestmove() -> String:
+ stockfish.go(depth)
+ var bestmove = yield(stockfish, "bestmove")
+ return bestmove.san
+
+ func kill() -> void:
+ stockfish.kill()
+ stockfish = null
diff --git a/ui/menus/LocalMultiplayer.tscn b/ui/menus/local_multiplayer/LocalMultiplayer.tscn
index 9aa4dd4..8b9c483 100644
--- a/ui/menus/LocalMultiplayer.tscn
+++ b/ui/menus/local_multiplayer/LocalMultiplayer.tscn
@@ -1,7 +1,7 @@
[gd_scene load_steps=4 format=2]
-[ext_resource path="res://ui/menus/LocalMultiplayer.gd" type="Script" id=1]
-[ext_resource path="res://ui/menus/lobby/GameConfig.tscn" type="PackedScene" id=2]
+[ext_resource path="res://ui/menus/local_multiplayer/LocalMultiplayer.gd" type="Script" id=1]
+[ext_resource path="res://ui/menus/local_multiplayer/GameConfig.tscn" type="PackedScene" id=2]
[sub_resource type="ButtonGroup" id=1]
@@ -19,11 +19,14 @@ margin_bottom = 427.0
[node name="GameConfig" parent="V" instance=ExtResource( 2 )]
unique_name_in_owner = true
visible = false
+anchor_left = 0.0
+anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
-margin_right = 351.0
-margin_bottom = 268.0
-color_config = false
+margin_left = 0.0
+margin_top = 0.0
+margin_right = 568.0
+margin_bottom = 250.0
button_group = SubResource( 1 )
[node name="PlayButton" type="Button" parent="V"]
diff --git a/ui/menus/local_multiplayer/NoEngineLabel.gd b/ui/menus/local_multiplayer/NoEngineLabel.gd
new file mode 100644
index 0000000..ea0ba46
--- /dev/null
+++ b/ui/menus/local_multiplayer/NoEngineLabel.gd
@@ -0,0 +1,27 @@
+extends RichTextLabel
+
+export var color: Color
+
+const vague_error = "[color=#%s][/color] You unable to use chess engine functionality."
+const web_noengine := """[color=#%s][/color] Your browser does not support [url=https://stockfishchess.org/]Stockfish[/url].
+Try  chrome for access to [url=https://stockfishchess.org/]Stockfish[/url]."""
+const desktop_noengine := """[color=#%s][/color] [url=https://stockfishchess.org/]Stockfish[/url] is not yet implemented for desktop.
+Try it on [url=https://bendn.itch.io/chess]web[/url] to use [url=https://stockfishchess.org/]Stockfish[/url]."""
+
+
+func _ready() -> void:
+ yield(get_tree(), "idle_frame")
+ if not visible: # engine exists yay
+ return
+
+ connect("meta_clicked", self, "open_url")
+ if OS.has_feature("JavaScript"):
+ append_bbcode(web_noengine % color.to_html())
+ elif OS.has_feature("pc"):
+ append_bbcode(desktop_noengine % color.to_html())
+ else:
+ append_bbcode(vague_error % color.to_html())
+
+
+func open_url(meta):
+ OS.shell_open(str(meta))
diff --git a/ui/menus/local_multiplayer/PlayerOptionButton.tscn b/ui/menus/local_multiplayer/PlayerOptionButton.tscn
new file mode 100644
index 0000000..67644c0
--- /dev/null
+++ b/ui/menus/local_multiplayer/PlayerOptionButton.tscn
@@ -0,0 +1,13 @@
+[gd_scene format=2]
+
+[node name="PlayerButton" type="OptionButton"]
+margin_left = 103.0
+margin_right = 253.0
+margin_bottom = 54.0
+rect_min_size = Vector2( 160, 0 )
+hint_tooltip = "Stockfish is a chess engine. It will play automatically."
+text = "Human"
+align = 1
+expand_icon = true
+items = [ "Human", null, false, 0, null, "Stockfish", null, false, 1, null ]
+selected = 0
diff --git a/ui/menus/local_multiplayer/color.tres b/ui/menus/local_multiplayer/color.tres
new file mode 100644
index 0000000..0e55d74
--- /dev/null
+++ b/ui/menus/local_multiplayer/color.tres
@@ -0,0 +1,3 @@
+[gd_resource type="ButtonGroup" format=2]
+
+[resource]
diff --git a/ui/menus/startmenu/StartMenu.tscn b/ui/menus/startmenu/StartMenu.tscn
index 3333b6e..1d72749 100644
--- a/ui/menus/startmenu/StartMenu.tscn
+++ b/ui/menus/startmenu/StartMenu.tscn
@@ -9,7 +9,7 @@
[ext_resource path="res://ui/menus/settings/Settings.tscn" type="PackedScene" id=7]
[ext_resource path="res://ui/menus/account/Account.tscn" type="PackedScene" id=8]
[ext_resource path="res://ui/menus/startmenu/VersionLabel.gd" type="Script" id=9]
-[ext_resource path="res://ui/menus/LocalMultiplayer.tscn" type="PackedScene" id=10]
+[ext_resource path="res://ui/menus/local_multiplayer/LocalMultiplayer.tscn" type="PackedScene" id=10]
[sub_resource type="DynamicFont" id=1]
size = 400
@@ -48,13 +48,13 @@ margin_top = 79.0
margin_right = -25.0
margin_bottom = -25.0
-[node name="local" parent="CenterContainer/tabs/" instance=ExtResource( 10 )]
+[node name="multiplayer" parent="CenterContainer/tabs/" instance=ExtResource( 5 )]
margin_left = 25.0
margin_top = 79.0
margin_right = -25.0
margin_bottom = -25.0
-[node name="multiplayer" parent="CenterContainer/tabs/" instance=ExtResource( 5 )]
+[node name="local" parent="CenterContainer/tabs/" instance=ExtResource( 10 )]
visible = false
margin_left = 25.0
margin_top = 79.0
diff --git a/ui/theme/main.theme b/ui/theme/main.theme
index de7e127..ae73fb9 100644
--- a/ui/theme/main.theme
+++ b/ui/theme/main.theme
Binary files differ
diff --git a/ui/theme/scrollbar/scroll.tres b/ui/theme/scrollbar/scroll.tres
index f7a69ac..769339f 100644
--- a/ui/theme/scrollbar/scroll.tres
+++ b/ui/theme/scrollbar/scroll.tres
@@ -1,8 +1,8 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
-content_margin_left = 2.0
-content_margin_right = 2.0
+content_margin_left = 5.0
+content_margin_right = 5.0
bg_color = Color( 0.027451, 0.211765, 0.258824, 1 )
corner_radius_top_left = 10
corner_radius_top_right = 10
diff --git a/ui/theme/slider/grabber.png b/ui/theme/slider/grabber.png
new file mode 100644
index 0000000..9b5d2dc
--- /dev/null
+++ b/ui/theme/slider/grabber.png
Binary files differ
diff --git a/ui/theme/slider/grabber.png.import b/ui/theme/slider/grabber.png.import
new file mode 100644
index 0000000..679e328
--- /dev/null
+++ b/ui/theme/slider/grabber.png.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/grabber.png-43fe852764a34b54674a226792085a5d.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://ui/theme/slider/grabber.png"
+dest_files=[ "res://.import/grabber.png-43fe852764a34b54674a226792085a5d.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=false
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0
diff --git a/ui/theme/slider/grabber_highlight.png b/ui/theme/slider/grabber_highlight.png
new file mode 100644
index 0000000..7edc323
--- /dev/null
+++ b/ui/theme/slider/grabber_highlight.png
Binary files differ
diff --git a/ui/theme/slider/grabber_highlight.png.import b/ui/theme/slider/grabber_highlight.png.import
new file mode 100644
index 0000000..922323a
--- /dev/null
+++ b/ui/theme/slider/grabber_highlight.png.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/grabber_highlight.png-849b6e6eff5166bd7f220287a77a5327.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://ui/theme/slider/grabber_highlight.png"
+dest_files=[ "res://.import/grabber_highlight.png-849b6e6eff5166bd7f220287a77a5327.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=false
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0