From e74f767cdbfff8ddee1e5f7cf8802121e3c3e7f5 Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Mon, 17 Mar 2025 23:30:30 -0500 Subject: [PATCH] added dynamic variant creation and unit definition for better special movement response by stockfish --- Systems/Cards/HorseCostume.gd | 2 +- Systems/Cards/KingsSquire.gd | 4 +- Systems/Cards/QueensSquire.gd | 2 +- .../FairyStockfishVariantGenerator.gd | 286 ++++++++++++++++++ .../FairyStockfishVariantGenerator.gd.uid | 1 + Systems/FairyStockfish/ServerManager.gd | 33 ++ Systems/FairyStockfish/StockfishClient.gd | 10 +- Systems/Game/ChessGame.gd | 57 +++- Systems/Game/ProgressionScreen.gd | 2 +- Systems/StateMachine/GameStates/BlackTurn.gd | 5 +- .../StateMachine/GameStates/CleanupPhase.gd | 2 + 11 files changed, 390 insertions(+), 14 deletions(-) create mode 100644 Systems/FairyStockfish/FairyStockfishVariantGenerator.gd create mode 100644 Systems/FairyStockfish/FairyStockfishVariantGenerator.gd.uid diff --git a/Systems/Cards/HorseCostume.gd b/Systems/Cards/HorseCostume.gd index 18b1c2b..99ae530 100644 --- a/Systems/Cards/HorseCostume.gd +++ b/Systems/Cards/HorseCostume.gd @@ -9,7 +9,7 @@ func _init(): description = "Attached Unit can move and capture in any direction" duration = 3 unitWhitelist = ["Pawn", "Bishop", "Queen", "Rook", "King"] - is_default = true + is_default = false # current_movement_string = "mWmFcfF" diff --git a/Systems/Cards/KingsSquire.gd b/Systems/Cards/KingsSquire.gd index 6231129..411948b 100644 --- a/Systems/Cards/KingsSquire.gd +++ b/Systems/Cards/KingsSquire.gd @@ -7,7 +7,7 @@ func _init(): rank = Rank.RANK_2 effectType = EffectType.MOVEMENT_MODIFIER description = "Attached Pawn can move and capture in any direction" - duration = 1 # Lasts for 2 turns + duration = 3 unitWhitelist = ["Pawn"] is_default = false # current_movement_string = "mWmFcfF" @@ -24,5 +24,5 @@ func apply_effect(target_piece = null, board_flow = null, game_state = null): func reset(): super.reset() - remaining_turns = duration + remaining_turns = duration attached_piece.reset_current_movement_string() diff --git a/Systems/Cards/QueensSquire.gd b/Systems/Cards/QueensSquire.gd index 9b53125..3d1c4ae 100644 --- a/Systems/Cards/QueensSquire.gd +++ b/Systems/Cards/QueensSquire.gd @@ -7,7 +7,7 @@ func _init(): rank = Rank.RANK_3 effectType = EffectType.MOVEMENT_MODIFIER description = "Attached Pawn can move in any direction, can only capture diagonally forward" - duration = 1 # Lasts for 2 turns + duration = 4 unitWhitelist = ["Pawn"] is_default = true # current_movement_string = "mWmFcfF" diff --git a/Systems/FairyStockfish/FairyStockfishVariantGenerator.gd b/Systems/FairyStockfish/FairyStockfishVariantGenerator.gd new file mode 100644 index 0000000..98d2340 --- /dev/null +++ b/Systems/FairyStockfish/FairyStockfishVariantGenerator.gd @@ -0,0 +1,286 @@ +extends Node +class_name FairyStockfishVariantGenerator + +const STANDARD_PIECE_CHARS = "PNBRQK" +const files = "abcdefghijkl" +const VARIANT_CHARS = "ACDEFGHIJLMOSTVWXYZ" + +func generate_variant_string(chess_game: ChessGame) -> String: + var width = chess_game.boardXSize + var height = chess_game.boardYSize + var prefix = "chessbuilder" + str(width) + "x" + str(height) + + var piece_data = gather_piece_data(chess_game) + var piece_definitions = piece_data.definitions + var piece_mappings = piece_data.mappings + var used_variant_chars = piece_data.used_chars + + var variant_string = "[" + prefix + ":chess]\n" + + # Build the pieceToCharTable with all used variant characters + var char_table = build_piece_char_table(used_variant_chars) + variant_string += "pieceToCharTable = " + char_table + "\n" + + variant_string += "maxRank = " + str(height) + "\n" + variant_string += "maxFile = " + files[width - 1] + "\n" + + var current_fen = chess_game.getCurrentFen() + variant_string += "startFen = " + current_fen + "\n\n" + + # Add wall types + variant_string += "walltype = * # Duck wall\n" + variant_string += "walltype = @ # Stone wall\n\n" + + # Add walling rules + variant_string += "wallingRule = static\n" + variant_string += "wallOrMove = false\n" + variant_string += "mobilityRegion = *:@\n" + variant_string += "prohibitedMove = *@\n\n" + + # Add the piece definitions + variant_string += "# Custom piece movement definitions\n" + for piece_key in piece_definitions: + variant_string += "piece " + piece_key + " = " + piece_definitions[piece_key] + "\n" + + # Add pieceDrops if needed for capturing + variant_string += "\n# Allow capturing with custom pieces\n" + variant_string += "pieceDrops = true\n" + + return variant_string + +# Build a complete pieceToCharTable that includes all variant characters +func build_piece_char_table(used_variant_chars: Array) -> String: + var char_table = STANDARD_PIECE_CHARS + + for variant_char in used_variant_chars: + char_table += variant_char.to_upper() + + char_table += "..*@..........." + + # Add lowercase versions + char_table += STANDARD_PIECE_CHARS.to_lower() + + for variant_char in used_variant_chars: + char_table += variant_char.to_lower() + + char_table += "..*@..........." + + return char_table + +# Gather all piece data from the current game state +func gather_piece_data(chess_game: ChessGame) -> Dictionary: + # Dictionary to track unique movement patterns + # Key format: "piece_char:color:movement_string" + var unique_movements = {} + + var modified_piece_types = {} + + # First pass: collect all unique movement patterns + for y in range(chess_game.boardYSize): + for x in range(chess_game.boardXSize): + var container = chess_game.boardContainer.get_node(str(x) + "-" + str(y)) as PieceContainer + if container && container.has_piece(): + var piece = container.get_piece() as Pawn + + if piece.current_movement_string != piece.original_movement_string: + var piece_char = get_piece_char(piece) + var color_key = "white" if piece.Item_Color == 0 else "black" + var piece_key = piece_char + ":" + color_key + + if !modified_piece_types.has(piece_key): + modified_piece_types[piece_key] = true + + var movement_key = piece_key + ":" + piece.current_movement_string + + if !unique_movements.has(movement_key): + unique_movements[movement_key] = { + "piece_char": piece_char, + "color": color_key, + "betza_notation": piece.current_movement_string, + "fairy_notation": convert_betza_to_fairy_stockfish(piece.current_movement_string), + "positions": [] + } + + unique_movements[movement_key].positions.append(str(x) + "-" + str(y)) + + var variant_char_index = 0 + var piece_definitions = {} + var piece_mappings = {} + var used_variant_chars = [] + + for movement_key in unique_movements: + var movement_data = unique_movements[movement_key] + var piece_char = movement_data.piece_char + var color = movement_data.color + var fs_notation = movement_data.fairy_notation + + var variant_char + if variant_char_index < VARIANT_CHARS.length(): + variant_char = VARIANT_CHARS[variant_char_index] + used_variant_chars.append(variant_char) + variant_char_index += 1 + else: + # If we run out of variant chars, we'll need to add more or implement another solution + print("Warning: Ran out of variant characters for piece definitions") + break + + # Store the piece definition + var definition_key = variant_char.to_upper() if color == "white" else variant_char.to_lower() + piece_definitions[definition_key] = fs_notation + + for pos in movement_data.positions: + piece_mappings[pos] = definition_key + + return { + "definitions": piece_definitions, + "mappings": piece_mappings, + "used_chars": used_variant_chars + } + +func get_piece_char(piece: Pawn) -> String: + match piece.name: + "Pawn": return "P" + "Knight": return "N" + "Bishop": return "B" + "Rook": return "R" + "Queen": return "Q" + "King": return "K" + _: return "P" # Default to pawn + +func convert_betza_to_fairy_stockfish(betza_notation: String) -> String: + var atoms = parse_betza_notation(betza_notation) + var fairy_stockfish_moves = [] + + for atom in atoms: + var move_type = atom.type + var modifiers = atom.modifiers + var range_limit = atom.range + + var move_only = "m" in modifiers + var capture_only = "c" in modifiers + + # Process direction modifiers + var direction_modifiers = [] + for modifier in modifiers: + if modifier.length() > 1 || ["f", "b", "l", "r"].has(modifier): + for char in modifier: + if ["f", "b", "l", "r"].has(char): + direction_modifiers.append(char) + + var fs_move = "" + + match move_type: + "W": fs_move += "W" # Wazir (orthogonal step) + "F": fs_move += "F" # Ferz (diagonal step) + "R": fs_move += "R" # Rook (orthogonal slider) + "B": fs_move += "B" # Bishop (diagonal slider) + "N": fs_move += "N" # Knight + "K": fs_move += "K" # King (one step in any direction) + "Q": fs_move += "Q" # Queen (combination of R and B) + + if range_limit > 0: + fs_move += str(range_limit) + + if direction_modifiers.size() > 0: + fs_move += "/" + for dir in direction_modifiers: + match dir: + "f": fs_move += "f" + "b": fs_move += "b" + "l": fs_move += "l" + "r": fs_move += "r" + + if move_only: + fs_move += "m" + elif capture_only: + fs_move += "c" + + fairy_stockfish_moves.append(fs_move) + var ret_str = "" + for moves in fairy_stockfish_moves: + ret_str += str(moves) + " " + + return ret_str + +# Parse Betza notation into atoms +func parse_betza_notation(notation: String) -> Array: + var atoms = [] + var i = 0 + + while i < notation.length(): + var atom = { + "type": "", + "modifiers": [], + "range": -1 # -1 means unlimited + } + + while i < notation.length() && ["m", "c"].has(notation[i]): + atom.modifiers.append(notation[i]) + i += 1 + + var directions = "" + while i < notation.length() && ["f", "b", "l", "r"].has(notation[i]): + directions += notation[i] + i += 1 + + if directions: + atom.modifiers.append(directions) + + # Get the atom type + if i < notation.length(): + atom.type = notation[i] + i += 1 + + # Check for range specification + var range_str = "" + while i < notation.length() && notation[i].is_valid_int(): + range_str += notation[i] + i += 1 + + if range_str: + atom.range = int(range_str) + + atoms.append(atom) + + return atoms + +# Generate a modified FEN string with custom piece characters +func generate_modified_fen(chess_game: ChessGame, piece_mappings: Dictionary) -> String: + var fen_parts = chess_game.getCurrentFen().split(" ") + var board_part = fen_parts[0] + var rows = board_part.split("/") + var new_rows = [] + + for y in range(rows.size()): + var new_row = "" + var x = 0 + + for c in rows[y]: + if c.is_valid_int(): + # Handle empty squares + new_row += c + x += int(c) + else: + var pos = str(x) + "-" + str(y) + if piece_mappings.has(pos): + new_row += piece_mappings[pos] + else: + new_row += c + x += 1 + + new_rows.append(new_row) + + fen_parts[0] = new_rows.join("/") + return fen_parts.join(" ") + +# Save the variant definition to a file +func save_variant_to_file(variant_string: String) -> bool: + print("VARIANT ", variant_string) + ServerManager.write_variant(variant_string) + return true + +# Helper function to get a complete variant definition for the current game state +func generate_and_save_variant(chess_game: ChessGame) -> String: + var variant_string = generate_variant_string(chess_game) + save_variant_to_file(variant_string) + return variant_string diff --git a/Systems/FairyStockfish/FairyStockfishVariantGenerator.gd.uid b/Systems/FairyStockfish/FairyStockfishVariantGenerator.gd.uid new file mode 100644 index 0000000..98dada0 --- /dev/null +++ b/Systems/FairyStockfish/FairyStockfishVariantGenerator.gd.uid @@ -0,0 +1 @@ +uid://boryt6vnd0icx diff --git a/Systems/FairyStockfish/ServerManager.gd b/Systems/FairyStockfish/ServerManager.gd index 70e680a..c60bdf1 100644 --- a/Systems/FairyStockfish/ServerManager.gd +++ b/Systems/FairyStockfish/ServerManager.gd @@ -3,6 +3,7 @@ extends Node var server_path: String = "" var log_dir: String = "" +var variant_dir : String = "" var log_string: String = "" var running := false var server_process_id: int = -50 @@ -12,6 +13,29 @@ var ping_http: HTTPRequest var server_url = "http://localhost:27531" + +func write_variant(variant_string: String): + + var file = FileAccess.open(variant_dir, FileAccess.WRITE_READ) + var error = FileAccess.get_open_error() + + if error != OK: + # print("Failed to open log file. Error code: ", error) + return + + if file == null: + # print("File handle is null") + return + + file.store_string(variant_string) + + var write_error = FileAccess.get_open_error() + if write_error != OK: + print("Failed to write to log file. Error code: ", write_error) + + file.close() + + func write_log(message: String): # First check if path is valid # print("Attempting to write to: ", log_dir) @@ -41,6 +65,7 @@ func write_log(message: String): func _ready(): server_path = extract_server_files() + "/ChessEngines/fairy-chess-server" setup_logging() + setup_variant() check_server_files(server_path) start_server() get_tree().set_auto_accept_quit(false) @@ -117,6 +142,14 @@ func setup_logging(): log_dir = l_dir.path_join("godot-chess.log") write_log("ServerManager initialized") +func setup_variant(): + var l_dir = get_globalDir() + "/variant" + + # Create directory if it doesn't exist + DirAccess.make_dir_recursive_absolute(l_dir) + + variant_dir = l_dir.path_join("custom_variant.ini") + write_log("ServerManager Variants initialized") func _exit_tree(): stop_server() diff --git a/Systems/FairyStockfish/StockfishClient.gd b/Systems/FairyStockfish/StockfishClient.gd index 62b1ec1..11f1866 100644 --- a/Systems/FairyStockfish/StockfishClient.gd +++ b/Systems/FairyStockfish/StockfishClient.gd @@ -92,8 +92,6 @@ func _exit_tree(): disconnect_engine(); -func update_position(fen: String): - load_fen(fen) @@ -121,7 +119,7 @@ func setVariant(variant: String = "8x8"): "options": [ { "name": "VariantPath", - "value": get_globalDir() + "/Assets" + "/ChessEngines/Fairy-Stockfish/src/variants.ini" + "value": get_globalDir() + "/variant" + "/custom_variant.ini" }, { "name": "UCI_Variant", @@ -148,7 +146,7 @@ func setElo(elo: int = 1350, variant: String = "8x8") -> void: "options": [ { "name": "VariantPath", - "value": get_globalDir() + "/Assets" + "/ChessEngines/Fairy-Stockfish/src/variants.ini" + "value": get_globalDir() + "/variant" + "/custom_variant.ini" }, { "name": "UCI_LimitStrength", @@ -188,7 +186,7 @@ func setElo(elo: int = 1350, variant: String = "8x8") -> void: func generateMove(think_time_ms: int = 1000) -> void: if not running: return - print("&&&&&&&&&&&&&&&GENERATING MOVE&&&&&&&&&&&&&&&&&&&&&&", str(game.getCurrentFen())) + print("&&&&&&&&&&&&&&&GENERATING MOVE&&&&&&&&&&&&&&&&&&&&&&", str(game.generate_variant_aware_fen())) move_time = think_time_ms @@ -196,7 +194,7 @@ func generateMove(think_time_ms: int = 1000) -> void: var body = JSON.stringify({ "movetime": think_time_ms, "depth": 15, - "fen": str(game.getCurrentFen()) + "fen": str(game.generate_variant_aware_fen()) }) # Request engine move diff --git a/Systems/Game/ChessGame.gd b/Systems/Game/ChessGame.gd index 7ef5351..3c57717 100644 --- a/Systems/Game/ChessGame.gd +++ b/Systems/Game/ChessGame.gd @@ -67,6 +67,7 @@ var winConditionManager: WinConditionManager = null var boss_type = null var boss_turn_additional = null var boss_turn_index = null +var fairyStockfishVariantGenerator = FairyStockfishVariantGenerator.new() @@ -153,6 +154,7 @@ func _on_new_game_requested(options = {}): initializeDeckSystem(mode == "vanilla") initializeBoard() initializeTiles() + fairyStockfishVariantGenerator.generate_and_save_variant(self) if cardDisplay: cardDisplay.visible = true if stateMachine: @@ -160,6 +162,7 @@ func _on_new_game_requested(options = {}): stateMachine.transitionToNextState(Constants.BLACK_TURN) else: initialize_game_system(mode == "vanilla") + fairyStockfishVariantGenerator.generate_and_save_variant(self) if currentFen: stockfishController.start_board(cpuElo, get_board_dimensions(currentFen)) else: @@ -903,8 +906,60 @@ func parseLocation(location: String) -> void: number += 1 locationY = location.substr(number + 1) +func generate_variant_aware_fen() -> String: + var piece_data = fairyStockfishVariantGenerator.gather_piece_data(self) + var piece_mappings = piece_data.mappings + + var fen = "" + + for y in range(boardYSize): + var emptySquares = 0 + for x in range(boardXSize): + var container = boardContainer.get_node(str(x) + "-" + str(y)) as PieceContainer + var pos_key = str(x) + "-" + str(y) + + if tileManager.active_tiles.has(pos_key): + var tile = tileManager.active_tiles[pos_key] + if tile is WallTile or tile is DoubleWallTile: + if emptySquares > 0: + fen += str(emptySquares) + emptySquares = 0 + fen += "*" # Wall character + continue + elif tile is PortalTile: + pass + + var piece = container.get_piece() + if piece == null: + emptySquares += 1 + else: + if emptySquares > 0: + fen += str(emptySquares) + emptySquares = 0 + + if piece_mappings.has(pos_key): + fen += piece_mappings[pos_key] + else: + fen += getPieceFenChar(piece) + + if emptySquares > 0: + fen += str(emptySquares) + + if y < boardYSize - 1: + fen += "/" + + fen += " %s %s %s %d %d" % [ + "b" if currentPlayer == BLACK else "w", + castlingRights, + enPassantTarget, + halfMoveClock, + moveCount + ] + + return fen + + func getCurrentFen() -> String: - # Generate FEN string from the current board position var fen = "" # For a standard chess board, we want to generate FEN from top (black side, rank 8) diff --git a/Systems/Game/ProgressionScreen.gd b/Systems/Game/ProgressionScreen.gd index d99768e..e3ed5ca 100644 --- a/Systems/Game/ProgressionScreen.gd +++ b/Systems/Game/ProgressionScreen.gd @@ -2,7 +2,7 @@ extends Control class_name ProgressionScreen signal save_pressed -signal deck_manager_visibility_changed(isvisible) +signal progression_screen_visibility_changed(isvisible) @onready var deckGrid = $MainContainer/GridScrollContainer/GridContainer @onready var bankContainer = $MainContainer/BankContainer/ScrollContainer/VBoxContainer diff --git a/Systems/StateMachine/GameStates/BlackTurn.gd b/Systems/StateMachine/GameStates/BlackTurn.gd index a4a2a02..60ba2e9 100644 --- a/Systems/StateMachine/GameStates/BlackTurn.gd +++ b/Systems/StateMachine/GameStates/BlackTurn.gd @@ -18,7 +18,8 @@ func enter(_previous: String, _data := {}) -> void: print("ENTERING STATE ", Constants.BLACK_TURN) game.currentPlayer = game.BLACK game.updateTurnIndicator() - + var variant = game.get_board_dimensions(game.currentFen) + game.stockfishController.setVariant(variant) # Delay to avoid duplication during animation if game.stockfishController: moveTimer.start(2) @@ -29,7 +30,7 @@ func _on_move_timer_timeout() -> void: if game.stockfishController: print("------------------STARTING GENERATING MOVE --------------------") - game.stockfishController.load_fen(game.getCurrentFen()) + game.stockfishController.load_fen(game.generate_variant_aware_fen()) OS.delay_msec(250) game.stockfishController.generateMove(1000) # 1 second think time stateDelay.start(2) diff --git a/Systems/StateMachine/GameStates/CleanupPhase.gd b/Systems/StateMachine/GameStates/CleanupPhase.gd index c6ce7b8..4ef7614 100644 --- a/Systems/StateMachine/GameStates/CleanupPhase.gd +++ b/Systems/StateMachine/GameStates/CleanupPhase.gd @@ -18,6 +18,8 @@ func enter(_previous: String, data := {}) -> void: if game.currentPlayer == game.WHITE and game.has_opponent: + var variant_file = game.fairyStockfishVariantGenerator.generate_and_save_variant(game) + # print("Variant Generated ", variant_file) finished.emit(Constants.BLACK_TURN) else: finished.emit(Constants.WHITE_TURN) \ No newline at end of file