Speed up the simulator 5-7x with identical games (6.10)

Precompute board adjacency and square sets, look up opponents once per
move, and cache walks that exclude only the start square. Every game is
unchanged for the same seed, checked with agents/scripts/fingerprint_games.py.
Plan: skip catch-up 6.9 (D44), stop running the random lineup (D45).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 17:02:53 +02:00
parent bbc95b6722
commit 0ca905eb71
8 changed files with 180 additions and 19 deletions
+69
View File
@@ -0,0 +1,69 @@
"""Check that an engine change leaves every game identical (events hashed per game).
Record before the change, check after (a rule change is expected to differ):
uv run python agents/scripts/fingerprint_games.py write /tmp/fingerprints.json
uv run python agents/scripts/fingerprint_games.py check /tmp/fingerprints.json
"""
import hashlib
import json
import sys
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from dojo.board import load_board
from dojo.engine import Game, RandomSource
from dojo.rules import RulesConfig
from dojo.sim import make_agent
BOARD = Path(__file__).resolve().parents[2] / "board"
GAMES = 60
CASES = [
("mixed4", RulesConfig(), ("balanced", "farmer", "aggressive", "balanced"), GAMES),
("casual4", RulesConfig(), ("casual",) * 4, GAMES),
(
"mixed6",
RulesConfig(),
("casual", "farmer", "aggressive", "balanced", "casual", "farmer"),
30,
),
(
"stop-r1",
RulesConfig(fatica_on_crossing=False, first_kumite_round=1),
("casual", "balanced", "farmer"),
30,
),
("random", RulesConfig(max_rounds=60), ("random", "casual", "random", "balanced"), 30),
]
def play(case: int, seed: int) -> tuple[str, int, str]:
name, config, bots, _ = CASES[case]
board = load_board(BOARD / "quadrant.txt", BOARD / "legend.toml")
agents = [make_agent(bot, seed, seat) for seat, bot in enumerate(bots)]
game = Game(board, config, agents, RandomSource(seed))
game.run()
digest = hashlib.sha256("\n".join(map(repr, game.events)).encode()).hexdigest()
return name, seed, digest
def main() -> None:
mode, path = sys.argv[1], Path(sys.argv[2])
tasks = [(i, seed) for i, case in enumerate(CASES) for seed in range(case[3])]
with ProcessPoolExecutor(8) as pool:
results = {f"{n}:{s}": d for n, s, d in pool.map(play, *zip(*tasks, strict=True))}
if mode == "write":
path.write_text(json.dumps(results, indent=0))
print(f"wrote {len(results)} games")
return
expected = json.loads(path.read_text())
bad = [k for k in expected if results.get(k) != expected[k]]
print(f"{len(expected) - len(bad)}/{len(expected)} games identical")
if bad:
print("differ:", bad[:10])
sys.exit(1)
if __name__ == "__main__":
main()