# 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()