Skip to content
Merged
Show file tree
Hide file tree
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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,13 @@ tunex -c configs/ironphoenix.json sprt
```

Tunex immediately prints the two engine paths and search settings, then
shows when each game starts, when its position reaches another 16 plies, and
as soon as each game finishes. In multi-worker tests, progress lines may be
interleaved; each line includes the game number. Game counters and W/D/L are
updated after each completed game; SPRT LLR, hypothesis boundaries, Elo
estimates and decisions are evaluated at **completed batch boundaries**.
shows when each game starts and as soon as each game finishes, without
printing repeated per-ply `Game ... playing | ply ...` messages.
In multi-worker tests, start and completion lines may be interleaved; each
includes the game number. Game counters and W/D/L are updated after each
completed game; SPRT LLR, hypothesis boundaries, Elo estimates and decisions
are evaluated at **completed batch boundaries**. Fixed matches retain
their existing per-ply progress messages.

Progress output is unbuffered (`flush=True`). For older installs, run
`git pull` and `python -m pip install -e .` before rerunning Tunex.
Expand Down
8 changes: 6 additions & 2 deletions src/tunex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ def command_probe(config: Config) -> int:
return 0


def _game_progress(label: str, lock: threading.Lock):
def _game_progress(
label: str, lock: threading.Lock, *, show_playing: bool = True
):
"""Progress callback is called on worker threads; serialize console lines."""
def report(spec: GameSpec, stage: str, plies: int) -> None:
if stage == "playing" and not show_playing:
return
with lock:
team = "Red+Yellow" if spec.candidate_team == 0 else "Blue+Green"
if stage == "starting":
Expand Down Expand Up @@ -136,7 +140,7 @@ def completed(record: GameRecord, batch_wdl: WDL) -> None:

batch_wdl, _ = runner.run(
games=batch, offset=played,
on_progress=_game_progress("SPRT", output_lock),
on_progress=_game_progress("SPRT", output_lock, show_playing=False),
on_game_complete=completed,
)
total.extend(batch_wdl)
Expand Down
30 changes: 27 additions & 3 deletions tests/test_live_progress.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Live progress covers games that have not finished and SPRT batches."""

import threading
from types import SimpleNamespace
from unittest.mock import patch

from tunex.cli import command_sprt
from tunex.cli import _game_progress, command_sprt
from tunex.config import SearchConfig, SprtConfig, VariantConfig
from tunex.engine import SearchResult
from tunex.match import MatchRunner
from tunex.match import GameSpec, MatchRunner


class FakeEngine:
Expand Down Expand Up @@ -87,6 +88,27 @@ def test_legacy_match_without_callback_does_not_change_engine_contract():
assert len(records) == 2


def test_sprt_quiet_progress_does_not_change_match_progress(capsys):
spec = GameSpec(game_id=7, candidate_team=1)
quiet = _game_progress("SPRT", threading.Lock(), show_playing=False)
quiet(spec, "starting", 0)
quiet(spec, "playing", 0)
quiet(spec, "playing", 16)
assert capsys.readouterr().out == (
"[SPRT] Game 8 starting | candidate Blue+Green\n"
)

verbose = _game_progress("Match", threading.Lock())
verbose(spec, "starting", 0)
verbose(spec, "playing", 0)
verbose(spec, "playing", 16)
assert capsys.readouterr().out == (
"[Match] Game 8 starting | candidate Blue+Green\n"
"[Match] Game 8 playing | ply 0\n"
"[Match] Game 8 playing | ply 16\n"
)


def test_sprt_prints_start_per_game_and_batch_decision(capsys, tmp_path):
cfg = config(workers=2, plies=17)
cfg.match.results_file = str(tmp_path / "games.jsonl")
Expand All @@ -100,7 +122,9 @@ def test_sprt_prints_start_per_game_and_batch_decision(capsys, tmp_path):
assert "workers=2 | go movetime 250" in output
assert "Game 1 starting" in output
assert "Game 2 starting" in output
assert "ply 16" in output
assert "[SPRT] Game 1 playing | ply" not in output
assert "[SPRT] Game 2 playing | ply" not in output
assert " playing | ply " not in output
assert "[SPRT] Completed 1/2" in output
assert "[SPRT] Completed 2/2" in output
assert "SPRT reached max_games" in output
Loading