2025, Dec 05 00:03

Почему в бесконечном анализе python-chess «скачет» таймер и как это исправить

Разбираем, почему в бесконечном анализе шахматного движка в GUI «скачет» таймер: чем режим python-chess отличается от одноразового и как держать одну сессию без сбросов.

Бесконечный анализ движка в шахматном GUI кажется простым: запустите движок, позвольте ему думать без ограничений и транслируйте по мере улучшения лучшую линию и оценку. На практике таймер на экране может «скакать» назад — что нелогично, когда ожидаешь только роста прошедшего времени. В этом руководстве объясняется, почему так происходит и как реализовать по-настоящему непрерывный анализ без сбросов времени.

Problem demonstration

Этот фрагмент запускает новый поток анализа на каждой итерации цикла и читает время, которое сообщает движок. Задумка — показывать монотонно растущее значение времени во время бесконечного анализа, но на деле показатель то растёт, то падает.

import queue
import time
import chess.engine
import threading
import chess

pos = chess.Board()
data_pipe = queue.Queue()
uci_engine = chess.engine.SimpleEngine.popen_uci("C:/Program Files/ChessX/data/engines/stockfish/stockfish-windows-x86-64.exe")
lines_cache = []

def run_probe():
    global pos
    global data_pipe
    global lines_cache
    global uci_engine
    with uci_engine.analysis(pos, chess.engine.Limit(time=None), multipv=1) as stream:
        for payload in stream:
            try:
                if "multipv" in payload and "pv" in payload:
                    lines_cache.append(payload)
                    if len(lines_cache) == 1:
                        with data_pipe.mutex:
                            data_pipe.queue.clear()
                        data_pipe.put(lines_cache)
                        lines_cache = []
            except chess.engine.EngineTerminatedError:
                break

while True:
    worker = threading.Thread(target=run_probe)
    worker.start()
    time.sleep(2)
    if not data_pipe.empty():
        batch = data_pipe.get()
        payload = batch[0]
        spent = payload.get("time")
        print(f"Engine thinking time: {spent:.3f} seconds")

Вывод «гуляет», потому что каждый новый поток запускает отдельную сессию анализа.

What’s actually going on

Каждый вызов uci_engine.analysis(...) открывает новый прогон анализа. Движок сообщает значение времени, которое соответствует лишь этому конкретному прогону. Когда вы на каждой итерации создаёте новый поток, анализ начинается с нуля. Таймер сбрасывается, поэтому отображение может перейти с 5 секунд обратно на 1–2 секунды и снова расти. Время не накапливается между прогонами, потому что вы не поддерживаете единую долгоживущую сессию.

Это отличается от одноразового интерфейса. В непрерывном режиме вы держите сессию анализа открытой и получаете поток обновлений InfoDict, пока движок продолжает думать. В одноразовом режиме вы задаёте фиксированный лимит и получаете единичный результат по его завершении.

analysis vs analyse

Есть два способа управлять движком, и от этого зависят поведение интерфейса и организация потоков. Интерфейс, который работает постоянно и отдаёт обновления по мере размышлений движка, предназначен для непрерывного анализа. Интерфейс, который запускается один раз с фиксированным временем или глубиной и возвращает единичный результат, подходит для разовых оценок. Если вам нужен монотонно растущий таймер, держите непрерывную сессию открытой, а не запускайте её заново снова и снова.

Используйте непрерывный поток, когда нужен бесконечный анализ с постоянными обновлениями InfoDict. Используйте одноразовый вызов, когда нужен единичный результат после фиксированного лимита.

Fix: keep a single analysis session alive

Запустите один фоновой рабочий поток, который открывает единый контекст анализа и постоянно отправляет обновления в очередь. Цикл вашего GUI должен лишь читать из этой очереди. Так вы избегаете сброса таймера и получаете желаемое поведение бесконечного анализа.

import queue
import time
import chess.engine
import threading
import chess

pos = chess.Board()
data_pipe = queue.Queue()
uci_engine = chess.engine.SimpleEngine.popen_uci("C:/Program Files/ChessX/data/engines/stockfish/stockfish-windows-x86-64.exe")

def analysis_worker():
    with uci_engine.analysis(pos, chess.engine.Limit(time=None), multipv=1) as stream:
        cache = []
        for payload in stream:
            if "multipv" in payload and "pv" in payload:
                cache.append(payload)
                if len(cache) == 1:
                    with data_pipe.mutex:
                        data_pipe.queue.clear()
                    data_pipe.put(cache)
                    cache = []

threading.Thread(target=analysis_worker, daemon=True).start()

while True:
    if not data_pipe.empty():
        batch = data_pipe.get()
        payload = batch[0]
        spent = payload.get("time")
        print(f"Engine thinking time: {spent:.3f} seconds")
    time.sleep(0.1)

Такой подход согласует отчёт движка о времени с вашими ожиданиями. Поскольку контекст анализа не пересоздаётся, движок продолжает думать бесконечно, а прошедшее время относится к одной непрерывной сессии.

Why this matters

В шахматном GUI целостность потока анализа критична. Перезапуск анализа портит не только таймер — начинают «прыгать» глубина, количество узлов и вывод pv. Одна долгоживущая сессия даёт стабильный, монотонный прогресс, за которым удобно следить в реальном времени, и упрощает модель потоков: есть ровно один источник обновлений от движка.

Takeaways

Если поле времени во время «бесконечного» анализа будто откатывается назад, вы, скорее всего, многократно переоткрываете анализ. Держите одну сессию открытой, передавайте её обновления InfoDict в интерфейс, а одноразовый вызов оставьте для оценок с фиксированным лимитом. Такая схема даёт ожидаемый таймер, который идёт только вверх, и ровный, непрерывный отклик движка.