@@ -374,7 +374,8 @@ class RunEngine(threading.Thread):
374374 def __init__ (self , eng_queue , engine_config_file , engine_path_and_file ,
375375 engine_id_name , max_depth = MAX_DEPTH ,
376376 base_ms = 300000 , inc_ms = 1000 , tc_type = 'fischer' ,
377- period_moves = 0 , is_stream_search_info = True ):
377+ period_moves = 0 , is_stream_search_info = True ,
378+ existing_engine = None ):
378379 """
379380 Run engine as opponent or as adviser.
380381
@@ -383,6 +384,8 @@ def __init__(self, eng_queue, engine_config_file, engine_path_and_file,
383384 :param engine_path_and_file:
384385 :param engine_id_name:
385386 :param max_depth:
387+ :param existing_engine: An existing chess.engine.SimpleEngine instance
388+ to reuse instead of spawning a new process.
386389 """
387390 threading .Thread .__init__ (self )
388391 self ._kill = threading .Event ()
@@ -398,7 +401,7 @@ def __init__(self, eng_queue, engine_config_file, engine_path_and_file,
398401 self .nps = 0
399402 self .max_depth = max_depth
400403 self .eng_queue = eng_queue
401- self .engine = None
404+ self .engine = existing_engine
402405 self .board = None
403406 self .analysis = is_stream_search_info
404407 self .is_nomove_number_in_variation = True
@@ -468,32 +471,34 @@ def run(self):
468471
469472 If there is error we still send bestmove None.
470473 """
471- folder = Path (self .engine_path_and_file )
472- folder = folder .parents [0 ]
474+ # Reuse existing engine if provided
475+ if self .engine is None :
476+ folder = Path (self .engine_path_and_file )
477+ folder = folder .parents [0 ]
473478
474- try :
475- if sys_os == 'Windows' :
476- self .engine = chess .engine .SimpleEngine .popen_uci (
477- self .engine_path_and_file , cwd = folder ,
478- creationflags = subprocess .CREATE_NO_WINDOW )
479- else :
480- self .engine = chess .engine .SimpleEngine .popen_uci (
481- self .engine_path_and_file , cwd = folder )
482- except chess .engine .EngineTerminatedError :
483- logging .warning ('Failed to start {}.' .format (self .engine_path_and_file ))
484- self .eng_queue .put ('bestmove {}' .format (self .bm ))
485- return
486- except Exception :
487- logging .exception ('Failed to start {}.' .format (
488- self .engine_path_and_file ))
489- self .eng_queue .put ('bestmove {}' .format (self .bm ))
490- return
479+ try :
480+ if sys_os == 'Windows' :
481+ self .engine = chess .engine .SimpleEngine .popen_uci (
482+ self .engine_path_and_file , cwd = folder ,
483+ creationflags = subprocess .CREATE_NO_WINDOW )
484+ else :
485+ self .engine = chess .engine .SimpleEngine .popen_uci (
486+ self .engine_path_and_file , cwd = folder )
487+ except chess .engine .EngineTerminatedError :
488+ logging .warning ('Failed to start {}.' .format (self .engine_path_and_file ))
489+ self .eng_queue .put ('bestmove {}' .format (self .bm ))
490+ return
491+ except Exception :
492+ logging .exception ('Failed to start {}.' .format (
493+ self .engine_path_and_file ))
494+ self .eng_queue .put ('bestmove {}' .format (self .bm ))
495+ return
491496
492- # Set engine option values
493- try :
494- self .configure_engine ()
495- except Exception :
496- logging .exception ('Failed to configure engine.' )
497+ # Set engine option values
498+ try :
499+ self .configure_engine ()
500+ except Exception :
501+ logging .exception ('Failed to configure engine.' )
497502
498503 # Set search limits
499504 if self .tc_type == 'delay' :
@@ -629,14 +634,25 @@ def run(self):
629634 logging .info (f'bestmove { self .bm } ' )
630635
631636 def quit_engine (self ):
632- """Quit engine."""
637+ """Quit engine.
638+
639+ Safe to call multiple times; subsequent calls are no-ops.
640+ """
641+ if self .engine is None :
642+ return
633643 logging .info ('quit engine' )
634644 try :
635645 self .engine .quit ()
636- except AttributeError :
637- logging .info ('AttributeError, self.engine is already None' )
638646 except Exception :
639647 logging .exception ('Failed to quit engine.' )
648+ self .engine = None
649+
650+ def get_engine (self ):
651+ """Return the engine instance without quitting it.
652+
653+ This allows the engine process to be reused across moves.
654+ """
655+ return self .engine
640656
641657 def short_variation_san (self ):
642658 """Returns variation in san but without move numbers."""
@@ -1695,6 +1711,9 @@ def play_game(self, window: sg.Window, board: chess.Board):
16951711 is_search_stop_for_resign = False
16961712 is_search_stop_for_user_wins = False
16971713 is_search_stop_for_user_draws = False
1714+
1715+ # Engine instance that persists across moves
1716+ persistent_engine = None
16981717 is_hide_book1 = True
16991718 is_hide_book2 = True
17001719 is_hide_search_info = True
@@ -2179,7 +2198,8 @@ def play_game(self, window: sg.Window, board: chess.Board):
21792198 self .queue , self .engine_config_file , self .opp_path_and_file ,
21802199 self .opp_id_name , self .max_depth , engine_timer .base ,
21812200 engine_timer .inc , tc_type = engine_timer .tc_type ,
2182- period_moves = board .fullmove_number
2201+ period_moves = board .fullmove_number ,
2202+ existing_engine = persistent_engine
21832203 )
21842204 search .get_board (board )
21852205 search .daemon = True
@@ -2287,7 +2307,8 @@ def play_game(self, window: sg.Window, board: chess.Board):
22872307 break
22882308
22892309 search .join ()
2290- search .quit_engine ()
2310+ # Keep engine alive for reuse; retrieve instance
2311+ persistent_engine = search .get_engine ()
22912312 is_book_from_gui = False
22922313
22932314 # If engine failed to send a legal move
@@ -2371,6 +2392,17 @@ def play_game(self, window: sg.Window, board: chess.Board):
23712392
23722393 # Auto-save game
23732394 logging .info ('Saving game automatically' )
2395+
2396+ # Quit the persistent engine now that the game is over or
2397+ # the user is exiting play mode (e.g. neutral, new game, resign).
2398+ if persistent_engine is not None :
2399+ logging .info ('Quitting persistent engine at end of game' )
2400+ try :
2401+ persistent_engine .quit ()
2402+ except Exception :
2403+ logging .exception ('Failed to quit persistent engine.' )
2404+ finally :
2405+ persistent_engine = None
23742406 if is_user_resigns :
23752407 self .game .headers ['Result' ] = '0-1' if self .is_user_white else '1-0'
23762408 self .game .headers ['Termination' ] = '{} resigns' .format (
0 commit comments