390 lines
12 KiB
GDScript
390 lines
12 KiB
GDScript
# Stockfish.gd
|
|
extends Node
|
|
|
|
# References to game objects
|
|
var board: Array
|
|
var game: ChessGame
|
|
|
|
# Engine state
|
|
var process_id: int = -1
|
|
var engine_thread: Thread
|
|
var pipe_info: Dictionary
|
|
var command_queue: Array = []
|
|
var response_queue: Array = []
|
|
var mutex: Mutex
|
|
var semaphore: Semaphore
|
|
var running := false
|
|
var engine_path: String = ""
|
|
var _cleanup_in_progress := false
|
|
var threadTracker: Array = []
|
|
var last_file_size = 0
|
|
# Game state
|
|
var moves: Array = []
|
|
var move_time: int = 1000 # in ms
|
|
var generated_move: Dictionary = {} # Stores the last generated move
|
|
|
|
# Piece type mapping
|
|
var symbol_from_piece_type := {
|
|
"PAWN": "p", "KNIGHT": "n", "BISHOP": "b",
|
|
"ROOK": "r", "QUEEN": "q", "KING": "k"
|
|
}
|
|
|
|
var piece_type_from_symbol := {
|
|
"p": "PAWN", "n": "KNIGHT", "b": "BISHOP",
|
|
"r": "ROOK", "q": "QUEEN", "k": "KING"
|
|
}
|
|
|
|
func _init(boardRef: Array):
|
|
board = boardRef
|
|
mutex = Mutex.new()
|
|
semaphore = Semaphore.new()
|
|
|
|
func _ready():
|
|
game = get_parent() as ChessGame
|
|
|
|
func _exit_tree():
|
|
disconnect_engine()
|
|
|
|
func _notification(what):
|
|
if what == NOTIFICATION_PREDELETE:
|
|
cleanup()
|
|
|
|
func cleanup():
|
|
if _cleanup_in_progress:
|
|
return
|
|
_cleanup_in_progress = true
|
|
|
|
disconnect_engine()
|
|
_cleanup_in_progress = false
|
|
|
|
func safe_cleanup():
|
|
if engine_thread:
|
|
if engine_thread.is_alive():
|
|
var timeout = 5000 # 5 seconds
|
|
var start_time = Time.get_ticks_msec()
|
|
while engine_thread.is_alive():
|
|
if Time.get_ticks_msec() - start_time > timeout:
|
|
printerr("Thread cleanup timeout")
|
|
break
|
|
OS.delay_msec(10)
|
|
var result = engine_thread.wait_to_finish()
|
|
if result != OK:
|
|
printerr("Failed to cleanup thread: ", result)
|
|
engine_thread = null
|
|
|
|
func connect_to_engine(path: String) -> bool:
|
|
if running:
|
|
return false
|
|
|
|
engine_path = path
|
|
|
|
pipe_info = OS.execute_with_pipe(engine_path, ["uci"])
|
|
process_id = pipe_info.get("pid", -1)
|
|
if process_id <= 0:
|
|
printerr("Failed to start engine process")
|
|
return false
|
|
|
|
running = true
|
|
print("PID ", process_id, " Connected to engine: ", engine_path)
|
|
|
|
# Start communication thread
|
|
engine_thread = Thread.new()
|
|
engine_thread.start(_engine_thread_function)
|
|
|
|
# # Initialize UCI mode
|
|
# _send_command_wait("uci", "uciok")
|
|
# _send_command_wait("isready", "readyok")
|
|
|
|
# # Initialize with current game state
|
|
_send_command("ucinewgame")
|
|
_send_command_wait("isready", "readyok")
|
|
load_fen(game.getCurrentFen())
|
|
return true
|
|
|
|
|
|
func disconnect_engine():
|
|
if running:
|
|
_send_command("quit")
|
|
running = false
|
|
|
|
# Wait for thread to finish
|
|
if engine_thread and engine_thread.is_started():
|
|
engine_thread.wait_to_finish()
|
|
|
|
# Clean up process
|
|
if pipe_info:
|
|
if pipe_info.get("stdio"):
|
|
pipe_info["stdio"].close()
|
|
if pipe_info.get("stderr"):
|
|
pipe_info["stderr"].close()
|
|
if process_id > 0:
|
|
OS.kill(process_id)
|
|
process_id = -1
|
|
|
|
print("Disconnected from engine")
|
|
|
|
func limit_strength_to(elo_value: int):
|
|
mutex.lock()
|
|
if elo_value != -1: # Using -1 instead of int.MaxValue
|
|
_send_command("setoption name UCI_LimitStrength value true")
|
|
_send_command("setoption name UCI_Elo value " + str(elo_value))
|
|
else:
|
|
_send_command("setoption name UCI_LimitStrength value false")
|
|
mutex.unlock()
|
|
|
|
func stop_calculating():
|
|
mutex.lock()
|
|
_send_command("stop")
|
|
mutex.unlock()
|
|
|
|
func load_fen(fen: String):
|
|
moves.clear()
|
|
update_position(fen)
|
|
|
|
func update_position(fen: String):
|
|
_send_command("position fen " + fen)
|
|
# _send_command_wait("isready", "readyok")
|
|
|
|
func generateMove(think_time_ms: int = 1000) -> void:
|
|
if not running:
|
|
return
|
|
print("------------=========GENERATE MOVE =========-----------------")
|
|
|
|
move_time = think_time_ms
|
|
|
|
# Send position
|
|
var command = "position fen " + game.getCurrentFen()
|
|
_send_command(command)
|
|
# _send_command_wait("isready", "readyok")
|
|
# var response = _send_command_wait("isready", "readyok")
|
|
# if not "readyok" in response:
|
|
# print("Engine not ready after setting position")
|
|
# return
|
|
|
|
# Get move with timeout for thinking
|
|
var response = _send_command_wait("go movetime " + str(move_time), "bestmove")
|
|
print("Move response: ", response)
|
|
|
|
# Parse response for bestmove
|
|
var lines = response.split("\n")
|
|
for line in lines:
|
|
if line.begins_with("bestmove"):
|
|
var parts = line.split(" ")
|
|
if parts.size() >= 4: # Should have "bestmove e2e4 ponder e7e5"
|
|
generated_move = {
|
|
"move": parts[1],
|
|
"ponder": parts[3]
|
|
}
|
|
print("Generated move: ", generated_move)
|
|
return
|
|
|
|
generated_move = {}
|
|
|
|
func getGeneratedMove() -> Dictionary:
|
|
var move = generated_move.duplicate()
|
|
generated_move.clear() # Clear after retrieving
|
|
return move
|
|
|
|
func from_move_to_string(move_data: Dictionary) -> String:
|
|
var board_size = len(board)
|
|
|
|
# Get source and target squares
|
|
var source_i = move_data.source % board_size
|
|
var source_j = move_data.source / board_size
|
|
var target_i = move_data.target % board_size
|
|
var target_j = move_data.target / board_size
|
|
|
|
# Convert to algebraic notation
|
|
var letters = "abcdefghijklmnopqrstuvwxyz".substr(0, board_size)
|
|
var str_move = "%s%d%s%d" % [
|
|
letters[source_i],
|
|
board_size - source_j,
|
|
letters[target_i],
|
|
board_size - target_j
|
|
]
|
|
|
|
# Add promotion piece if needed
|
|
if move_data.get("flags", "") == "PROMOTION":
|
|
str_move += symbol_from_piece_type[move_data.promotion_piece]
|
|
|
|
return str_move
|
|
|
|
func send_move(move_data: Dictionary):
|
|
var str_move = from_move_to_string(move_data)
|
|
moves.append(str_move)
|
|
print("move: ", str_move)
|
|
|
|
# Update engine with the new move
|
|
mutex.lock()
|
|
var command = "position fen " + game.getCurrentFen()
|
|
if moves.size() > 0:
|
|
command += " moves " + " ".join(moves)
|
|
_send_command(command)
|
|
mutex.unlock()
|
|
|
|
|
|
|
|
func safe_get_line(stdio: FileAccess) -> Array:
|
|
var start_time = Time.get_ticks_msec()
|
|
var timeout = 500 # 50ms timeout
|
|
|
|
if stdio and not stdio.eof_reached():
|
|
var thread = Thread.new()
|
|
var mutex = Mutex.new()
|
|
var done = false
|
|
var result = ""
|
|
|
|
thread.start(func():
|
|
# Try to read a chunk of data
|
|
var buffer = stdio.get_buffer(1024) # Read up to 1KB at a time
|
|
if buffer.size() > 0:
|
|
var content = buffer.get_string_from_utf8()
|
|
var lines = content.split("\n")
|
|
# Get last non-empty line
|
|
# var last_line = ""
|
|
# for i in range(lines.size() - 1, -1, -1):
|
|
# if not lines[i].strip_edges().is_empty():
|
|
# last_line = lines[i]
|
|
# break
|
|
|
|
mutex.lock()
|
|
# result = last_line
|
|
done = true
|
|
mutex.unlock()
|
|
print("THREAD: ", lines)
|
|
return lines
|
|
|
|
mutex.lock()
|
|
done = true
|
|
mutex.unlock()
|
|
return []
|
|
)
|
|
|
|
# Wait for either completion or timeout
|
|
while true:
|
|
mutex.lock()
|
|
var is_done = done
|
|
mutex.unlock()
|
|
|
|
if not thread.is_alive():
|
|
print("DeadThread")
|
|
var thread_result = thread.wait_to_finish()
|
|
print("******T-RES*********", thread_result)
|
|
return [thread_result, null]
|
|
# else:
|
|
# print("Alive THread Kill ME")
|
|
|
|
if Time.get_ticks_msec() - start_time > timeout:
|
|
break
|
|
|
|
OS.delay_msec(200)
|
|
|
|
# Handle thread cleanup and result capture
|
|
if thread.is_alive():
|
|
threadTracker.push_front(thread)
|
|
mutex.lock()
|
|
var final_result = [result, null]
|
|
mutex.unlock()
|
|
return final_result
|
|
else:
|
|
var thread_result = thread.wait_to_finish()
|
|
return [thread_result, null]
|
|
|
|
return ["", null]
|
|
|
|
func _engine_thread_function():
|
|
print("Engine thread started")
|
|
var stdio = pipe_info.get("stdio")
|
|
var stderr = pipe_info.get("stderr")
|
|
|
|
|
|
while running:
|
|
mutex.lock()
|
|
print("CMDS ", command_queue)
|
|
if command_queue.size() > 0:
|
|
var command = command_queue.pop_front()
|
|
mutex.unlock()
|
|
|
|
if stdio:
|
|
stdio.store_line(command)
|
|
print("************************Sent command: ", command)
|
|
OS.delay_msec(200)
|
|
else:
|
|
mutex.unlock()
|
|
|
|
print("safe_get_line Start")
|
|
var result = safe_get_line(stdio)
|
|
print("safe_get_line End", result)
|
|
|
|
# var line = result[0]
|
|
# if not line.is_empty():
|
|
# mutex.lock()
|
|
# response_queue.push_back(line)
|
|
# mutex.unlock()
|
|
# semaphore.post()
|
|
# print("Engine response: ", line)
|
|
# var line = result[0]
|
|
for line in result:
|
|
if line and not line.is_empty():
|
|
mutex.lock()
|
|
response_queue.append_array(line)
|
|
mutex.unlock()
|
|
semaphore.post()
|
|
print("Engine response: ", line)
|
|
|
|
OS.delay_msec(200)
|
|
|
|
print("################################left thread###########################")
|
|
|
|
func _send_command_wait(command: String, expected: String = "") -> String:
|
|
# Add command to queue more efficiently
|
|
mutex.lock()
|
|
command_queue.push_back(command)
|
|
mutex.unlock()
|
|
|
|
var response = ""
|
|
var start_time = Time.get_ticks_msec()
|
|
var timeout = 5000 # Reduced timeout to 5 seconds
|
|
|
|
# Use an exponential backoff for delays
|
|
var delay_time = 1
|
|
var max_delay = 16 # Maximum delay in milliseconds
|
|
|
|
while running:
|
|
var current_time = Time.get_ticks_msec()
|
|
if current_time - start_time > timeout:
|
|
printerr("Command timeout: ", command)
|
|
break
|
|
|
|
if semaphore.try_wait():
|
|
# Reset delay on successful response
|
|
delay_time = 1
|
|
|
|
mutex.lock()
|
|
var lines_to_process = response_queue.duplicate()
|
|
response_queue.clear()
|
|
mutex.unlock()
|
|
print("PROCESSING LINES ##################")
|
|
print(lines_to_process)
|
|
# if lines_to_process.is_empty():
|
|
# return
|
|
# Process all available lines
|
|
for line in lines_to_process:
|
|
if not line.is_empty():
|
|
print(expected, " =? ", line)
|
|
if expected.is_empty() or expected in line:
|
|
print("MATCHED LINES ##################", line, ' ', expected)
|
|
return line
|
|
# print("This should not print if we returned")
|
|
else:
|
|
# Use exponential backoff for delays
|
|
OS.delay_msec(delay_time)
|
|
delay_time = mini(delay_time * 2, max_delay)
|
|
|
|
return response
|
|
|
|
func _send_command(command: String):
|
|
mutex.lock()
|
|
command_queue.push_back(command)
|
|
mutex.unlock()
|
|
|