online multiplayer chess game (note server currently down)
STOCKFISH
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 {} @@ -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 @@ -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} @@ -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 @@ -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 Binary files differindex de7e127..ae73fb9 100644 --- a/ui/theme/main.theme +++ b/ui/theme/main.theme 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 Binary files differnew file mode 100644 index 0000000..9b5d2dc --- /dev/null +++ b/ui/theme/slider/grabber.png 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 Binary files differnew file mode 100644 index 0000000..7edc323 --- /dev/null +++ b/ui/theme/slider/grabber_highlight.png 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 |