added dynamic variant creation and unit definition for better special movement response by stockfish

This commit is contained in:
2ManyProjects 2025-03-17 23:30:30 -05:00
parent 860c3d31a7
commit e74f767cdb
11 changed files with 390 additions and 14 deletions

View file

@ -9,7 +9,7 @@ func _init():
description = "Attached Unit can move and capture in any direction" description = "Attached Unit can move and capture in any direction"
duration = 3 duration = 3
unitWhitelist = ["Pawn", "Bishop", "Queen", "Rook", "King"] unitWhitelist = ["Pawn", "Bishop", "Queen", "Rook", "King"]
is_default = true is_default = false
# current_movement_string = "mWmFcfF" # current_movement_string = "mWmFcfF"

View file

@ -7,7 +7,7 @@ func _init():
rank = Rank.RANK_2 rank = Rank.RANK_2
effectType = EffectType.MOVEMENT_MODIFIER effectType = EffectType.MOVEMENT_MODIFIER
description = "Attached Pawn can move and capture in any direction" description = "Attached Pawn can move and capture in any direction"
duration = 1 # Lasts for 2 turns duration = 3
unitWhitelist = ["Pawn"] unitWhitelist = ["Pawn"]
is_default = false is_default = false
# current_movement_string = "mWmFcfF" # current_movement_string = "mWmFcfF"

View file

@ -7,7 +7,7 @@ func _init():
rank = Rank.RANK_3 rank = Rank.RANK_3
effectType = EffectType.MOVEMENT_MODIFIER effectType = EffectType.MOVEMENT_MODIFIER
description = "Attached Pawn can move in any direction, can only capture diagonally forward" description = "Attached Pawn can move in any direction, can only capture diagonally forward"
duration = 1 # Lasts for 2 turns duration = 4
unitWhitelist = ["Pawn"] unitWhitelist = ["Pawn"]
is_default = true is_default = true
# current_movement_string = "mWmFcfF" # current_movement_string = "mWmFcfF"

View file

@ -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

View file

@ -0,0 +1 @@
uid://boryt6vnd0icx

View file

@ -3,6 +3,7 @@ extends Node
var server_path: String = "" var server_path: String = ""
var log_dir: String = "" var log_dir: String = ""
var variant_dir : String = ""
var log_string: String = "" var log_string: String = ""
var running := false var running := false
var server_process_id: int = -50 var server_process_id: int = -50
@ -12,6 +13,29 @@ var ping_http: HTTPRequest
var server_url = "http://localhost:27531" 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): func write_log(message: String):
# First check if path is valid # First check if path is valid
# print("Attempting to write to: ", log_dir) # print("Attempting to write to: ", log_dir)
@ -41,6 +65,7 @@ func write_log(message: String):
func _ready(): func _ready():
server_path = extract_server_files() + "/ChessEngines/fairy-chess-server" server_path = extract_server_files() + "/ChessEngines/fairy-chess-server"
setup_logging() setup_logging()
setup_variant()
check_server_files(server_path) check_server_files(server_path)
start_server() start_server()
get_tree().set_auto_accept_quit(false) get_tree().set_auto_accept_quit(false)
@ -117,6 +142,14 @@ func setup_logging():
log_dir = l_dir.path_join("godot-chess.log") log_dir = l_dir.path_join("godot-chess.log")
write_log("ServerManager initialized") 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(): func _exit_tree():
stop_server() stop_server()

View file

@ -92,8 +92,6 @@ func _exit_tree():
disconnect_engine(); disconnect_engine();
func update_position(fen: String):
load_fen(fen)
@ -121,7 +119,7 @@ func setVariant(variant: String = "8x8"):
"options": [ "options": [
{ {
"name": "VariantPath", "name": "VariantPath",
"value": get_globalDir() + "/Assets" + "/ChessEngines/Fairy-Stockfish/src/variants.ini" "value": get_globalDir() + "/variant" + "/custom_variant.ini"
}, },
{ {
"name": "UCI_Variant", "name": "UCI_Variant",
@ -148,7 +146,7 @@ func setElo(elo: int = 1350, variant: String = "8x8") -> void:
"options": [ "options": [
{ {
"name": "VariantPath", "name": "VariantPath",
"value": get_globalDir() + "/Assets" + "/ChessEngines/Fairy-Stockfish/src/variants.ini" "value": get_globalDir() + "/variant" + "/custom_variant.ini"
}, },
{ {
"name": "UCI_LimitStrength", "name": "UCI_LimitStrength",
@ -188,7 +186,7 @@ func setElo(elo: int = 1350, variant: String = "8x8") -> void:
func generateMove(think_time_ms: int = 1000) -> void: func generateMove(think_time_ms: int = 1000) -> void:
if not running: if not running:
return return
print("&&&&&&&&&&&&&&&GENERATING MOVE&&&&&&&&&&&&&&&&&&&&&&", str(game.getCurrentFen())) print("&&&&&&&&&&&&&&&GENERATING MOVE&&&&&&&&&&&&&&&&&&&&&&", str(game.generate_variant_aware_fen()))
move_time = think_time_ms move_time = think_time_ms
@ -196,7 +194,7 @@ func generateMove(think_time_ms: int = 1000) -> void:
var body = JSON.stringify({ var body = JSON.stringify({
"movetime": think_time_ms, "movetime": think_time_ms,
"depth": 15, "depth": 15,
"fen": str(game.getCurrentFen()) "fen": str(game.generate_variant_aware_fen())
}) })
# Request engine move # Request engine move

View file

@ -67,6 +67,7 @@ var winConditionManager: WinConditionManager = null
var boss_type = null var boss_type = null
var boss_turn_additional = null var boss_turn_additional = null
var boss_turn_index = null var boss_turn_index = null
var fairyStockfishVariantGenerator = FairyStockfishVariantGenerator.new()
@ -153,6 +154,7 @@ func _on_new_game_requested(options = {}):
initializeDeckSystem(mode == "vanilla") initializeDeckSystem(mode == "vanilla")
initializeBoard() initializeBoard()
initializeTiles() initializeTiles()
fairyStockfishVariantGenerator.generate_and_save_variant(self)
if cardDisplay: if cardDisplay:
cardDisplay.visible = true cardDisplay.visible = true
if stateMachine: if stateMachine:
@ -160,6 +162,7 @@ func _on_new_game_requested(options = {}):
stateMachine.transitionToNextState(Constants.BLACK_TURN) stateMachine.transitionToNextState(Constants.BLACK_TURN)
else: else:
initialize_game_system(mode == "vanilla") initialize_game_system(mode == "vanilla")
fairyStockfishVariantGenerator.generate_and_save_variant(self)
if currentFen: if currentFen:
stockfishController.start_board(cpuElo, get_board_dimensions(currentFen)) stockfishController.start_board(cpuElo, get_board_dimensions(currentFen))
else: else:
@ -903,8 +906,60 @@ func parseLocation(location: String) -> void:
number += 1 number += 1
locationY = location.substr(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: func getCurrentFen() -> String:
# Generate FEN string from the current board position
var fen = "" var fen = ""
# For a standard chess board, we want to generate FEN from top (black side, rank 8) # For a standard chess board, we want to generate FEN from top (black side, rank 8)

View file

@ -2,7 +2,7 @@ extends Control
class_name ProgressionScreen class_name ProgressionScreen
signal save_pressed signal save_pressed
signal deck_manager_visibility_changed(isvisible) signal progression_screen_visibility_changed(isvisible)
@onready var deckGrid = $MainContainer/GridScrollContainer/GridContainer @onready var deckGrid = $MainContainer/GridScrollContainer/GridContainer
@onready var bankContainer = $MainContainer/BankContainer/ScrollContainer/VBoxContainer @onready var bankContainer = $MainContainer/BankContainer/ScrollContainer/VBoxContainer

View file

@ -18,7 +18,8 @@ func enter(_previous: String, _data := {}) -> void:
print("ENTERING STATE ", Constants.BLACK_TURN) print("ENTERING STATE ", Constants.BLACK_TURN)
game.currentPlayer = game.BLACK game.currentPlayer = game.BLACK
game.updateTurnIndicator() game.updateTurnIndicator()
var variant = game.get_board_dimensions(game.currentFen)
game.stockfishController.setVariant(variant)
# Delay to avoid duplication during animation # Delay to avoid duplication during animation
if game.stockfishController: if game.stockfishController:
moveTimer.start(2) moveTimer.start(2)
@ -29,7 +30,7 @@ func _on_move_timer_timeout() -> void:
if game.stockfishController: if game.stockfishController:
print("------------------STARTING GENERATING MOVE --------------------") print("------------------STARTING GENERATING MOVE --------------------")
game.stockfishController.load_fen(game.getCurrentFen()) game.stockfishController.load_fen(game.generate_variant_aware_fen())
OS.delay_msec(250) OS.delay_msec(250)
game.stockfishController.generateMove(1000) # 1 second think time game.stockfishController.generateMove(1000) # 1 second think time
stateDelay.start(2) stateDelay.start(2)

View file

@ -18,6 +18,8 @@ func enter(_previous: String, data := {}) -> void:
if game.currentPlayer == game.WHITE and game.has_opponent: 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) finished.emit(Constants.BLACK_TURN)
else: else:
finished.emit(Constants.WHITE_TURN) finished.emit(Constants.WHITE_TURN)