added dynamic variant creation and unit definition for better special movement response by stockfish
This commit is contained in:
parent
860c3d31a7
commit
e74f767cdb
11 changed files with 390 additions and 14 deletions
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
286
Systems/FairyStockfish/FairyStockfishVariantGenerator.gd
Normal file
286
Systems/FairyStockfish/FairyStockfishVariantGenerator.gd
Normal 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
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://boryt6vnd0icx
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in a new issue