Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 84 additions & 49 deletions python_easy_chess_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
BOX_TITLE = f'{APP_NAME} {APP_VERSION}'
REVIEW_MAX_DISPLAY_GAMES = 10000
REVIEW_ANALYSIS_MULTIPV_LINES = 3
REVIEW_ANALYSIS_PV_MOVES = 7
REVIEW_ANALYSIS_PV_MOVES = 7
REVIEW_NAV_DEBOUNCE_SEC = 0.3
REVIEW_MOVE_LIST_HEIGHT = 11
REVIEW_ANALYSIS_BOX_HEIGHT = 4

Expand Down Expand Up @@ -555,10 +556,13 @@ def run(self):
except Exception:
logging.exception('Failed to configure runtime analysis options.')

# Set search limits
# Set search limits.
# For infinite analysis pass limit=None so that python-chess sends
# "go infinite" to the engine (Limit() is truthy and would produce
# a bare "go" without the infinite token).
if self.tc_type == 'infinite':
limit = chess.engine.Limit(
depth=self.max_depth if self.max_depth != MAX_DEPTH else None)
limit = (chess.engine.Limit(depth=self.max_depth)
if self.max_depth != MAX_DEPTH else None)
elif self.tc_type == 'delay':
limit = chess.engine.Limit(
depth=self.max_depth if self.max_depth != MAX_DEPTH else None,
Expand Down Expand Up @@ -690,13 +694,19 @@ def run(self):
# Apply engine move delay if movetime is small
if self.is_move_delay:
while True:
if time.perf_counter() - start_time >= self.move_delay_sec:
if (self._kill.is_set()
or time.perf_counter() - start_time
>= self.move_delay_sec):
break
logging.info('Delay sending of best move {}'.format(self.bm))
time.sleep(1.0)

# If bm is None, we will use engine.play()
if self.bm is None:
# Skip this fallback when the search was explicitly interrupted
# to avoid blocking the thread with an unconstrained engine call.
# Also skip when limit is None (infinite analysis) since
# engine.play() requires a concrete Limit object.
if self.bm is None and not self._kill.is_set() and limit is not None:
logging.info('bm is none, we will try engine,play().')
try:
result = self.engine.play(self.board, limit)
Expand Down Expand Up @@ -829,8 +839,9 @@ def reset_review_state(self):
self.review_analysis_lines = [''] * REVIEW_ANALYSIS_MULTIPV_LINES
self.review_analysis_enabled = False
self.review_analysis_status = 'Analysis stopped'
self.review_analysis_search = None
self.review_analysis_engine = None
self.review_analysis_search = None
self.review_analysis_engine = None
self.review_nav_last_time = 0

def update_game(self, mc: int, user_move: str, time_left: int, user_comment: str):
"""Saves moves in the game.
Expand Down Expand Up @@ -2929,41 +2940,46 @@ def refresh_review_analysis(self, window):
return
self.start_review_analysis(window)

def poll_review_analysis(self, window):
"""Consume engine messages for Review mode analysis."""
updated = False
while True:
try:
msg = self.review_queue.get_nowait()
except queue.Empty:
break
except Exception:
logging.exception('Failed to read Review mode analysis queue.')
break

msg_str = str(msg)
if 'multipv_info' in msg_str:
try:
line_no, info_line = msg_str.split(' | ', 1)
line_number = int(line_no.strip())
if not 1 <= line_number <= REVIEW_ANALYSIS_MULTIPV_LINES:
raise ValueError('Invalid MultiPV line number')
line_index = line_number - 1
info_line = info_line.rsplit(' multipv_info', 1)[0]
self.review_analysis_lines[line_index] = \
self.shorten_review_analysis_line(info_line)
updated = True
except Exception:
logging.exception('Failed to parse Review mode analysis info.')
elif 'bestmove' in msg_str:
if self.review_analysis_search is not None:
self.review_analysis_search.join()
self.review_analysis_engine = \
self.review_analysis_search.get_engine()
self.review_analysis_search = None
if self.review_analysis_enabled:
self.review_analysis_status = \
'Analysis ready - {}'.format(self.analysis_id_name)
def poll_review_analysis(self, window):
"""Consume engine messages for Review mode analysis."""
updated = False
is_debouncing = bool(self.review_nav_last_time)
while True:
try:
msg = self.review_queue.get_nowait()
except queue.Empty:
break
except Exception:
logging.exception('Failed to read Review mode analysis queue.')
break

msg_str = str(msg)
if 'multipv_info' in msg_str:
# Skip stale analysis info from the old position while
# waiting for the debounce to restart analysis.
if is_debouncing:
continue
try:
line_no, info_line = msg_str.split(' | ', 1)
line_number = int(line_no.strip())
if not 1 <= line_number <= REVIEW_ANALYSIS_MULTIPV_LINES:
raise ValueError('Invalid MultiPV line number')
line_index = line_number - 1
info_line = info_line.rsplit(' multipv_info', 1)[0]
self.review_analysis_lines[line_index] = \
self.shorten_review_analysis_line(info_line)
updated = True
except Exception:
logging.exception('Failed to parse Review mode analysis info.')
elif 'bestmove' in msg_str:
if self.review_analysis_search is not None:
self.review_analysis_search.join()
self.review_analysis_engine = \
self.review_analysis_search.get_engine()
self.review_analysis_search = None
if self.review_analysis_enabled and not is_debouncing:
self.review_analysis_status = \
'Analysis ready - {}'.format(self.analysis_id_name)
updated = True

if updated:
Expand Down Expand Up @@ -3107,9 +3123,17 @@ def start_review_mode(self, window):
button, value = review_window.Read(timeout=50)
self.poll_review_analysis(review_window)

# Skip timeout events as analysis updates are processed by
# poll_review_analysis() called earlier in the loop.
if button == sg.TIMEOUT_KEY:
# Skip timeout events as analysis updates are processed by
# poll_review_analysis() called earlier in the loop.
if button == sg.TIMEOUT_KEY:
# Restart analysis after debounce delay following navigation.
nav_time = self.review_nav_last_time
if (nav_time
and self.review_analysis_enabled
and time.time() - nav_time
>= REVIEW_NAV_DEBOUNCE_SEC):
self.review_nav_last_time = 0
self.start_review_analysis(review_window)
continue

if button is None:
Expand Down Expand Up @@ -3200,9 +3224,20 @@ def start_review_mode(self, window):
self.update_review_window(review_window)
continue

if position_changed:
self.update_review_window(review_window)
self.refresh_review_analysis(review_window)
if position_changed:
self.update_review_window(review_window)
if self.review_analysis_enabled:
# Signal the analysis thread to stop without blocking.
# The actual join and restart happen in the debounce
# handler after the user stops pressing buttons.
if self.review_analysis_search is not None:
self.review_analysis_search.stop()
self.review_nav_last_time = time.time()
self.review_analysis_lines = [''] * REVIEW_ANALYSIS_MULTIPV_LINES
self.review_analysis_status = 'Waiting...'
self.update_review_analysis_panel(review_window)
else:
self.refresh_review_analysis(review_window)

self.close_review_analysis()
review_window.Close()
Expand Down