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:
@@ -60,6 +60,9 @@ Deliverables:
|
||||
| D41 | Phase 6 order: board and Fatica (6.4 with 6.5) → seat order (6.2) → catch-up (6.9) → style dynamics (6.3) → Kiai (6.6), player counts (6.7), length → consolidation (6.8). Structural fixes before tuning | 6.1 review. |
|
||||
| D42 | **Board draft 2 adopted** as `board/quadrant.txt` (`board/board.txt` regenerated; draft 1 is in git history, commit 4998931) | 6.4.0 results. |
|
||||
| D43 | **6.2 seat order suspended**: measuring it needs 4,000+ games per scenario for an effect of a few points; D35 (no Kumite in round 1) stays in the rules. Seat rates are still reported by every run and watched in 6.8 | Developer, after the 4,000-game runs were started. |
|
||||
| D44 | **6.9 catch-up skipped**: the v0.8 baseline already has 3–4% of player-turns out of contention (worst player 10–13%), so candidate A has little to fix | Developer, after the v0.8 baseline metrics. |
|
||||
| D45 | The **`random` lineup is no longer run** in experiments: it never finishes a game and cost ~80% of a run's time. The bot stays in the code and the tests | Developer, 6.10. |
|
||||
| D46 | Performance work must leave **every game identical** (same events for the same seed), so every result recorded in this file stays valid | Developer, 6.10. |
|
||||
|
||||
## Phases
|
||||
|
||||
@@ -170,7 +173,7 @@ Deliverables:
|
||||
- One change at a time. When a change is adopted, re-run the 6.1 baseline so later steps start from the current rules, and log the decision (D-number) here and in `RULES.md`.
|
||||
- Results go in this file as a table per step (command line, scenario, key measures).
|
||||
|
||||
**Next action:** 6.9 catch-up candidate A, when the developer says to continue. 6.2 is suspended (D43); 6.4.1 (chance square) stays optional. Order of the steps: D41. Everything up to D43 and RULES.md v0.8 is committed; commit only when the developer asks. 6.0, 6.1, D35 and D36 are not committed yet; commit only when the developer asks.
|
||||
**Next action:** 6.3 style dynamics, when the developer says to continue. 6.2 is suspended (D43), 6.9 skipped (D44); 6.4.1 (chance square) stays optional. Order of the steps: D41. Everything up to D43 and RULES.md v0.8 is committed (bbc95b6); 6.10 and D44–D46 are not; commit only when the developer asks.
|
||||
|
||||
**Targets** (confirmed by the developer as a starting point, D32):
|
||||
|
||||
@@ -266,8 +269,14 @@ Deliverables:
|
||||
- [x] 6.5 Home farming: covered by board draft 2 (6.4.0): Allenamento moved from the south training room onto the Dojo ring; own-quadrant share of square income 74% → 62–67%, short of the 60% aim (players cross their own quadrant for most of the game); draft 2 adopted as is (D42). Original step: after 6.4, if the own-quadrant share of square income is still above 60% (baseline: 71%), move Allenamento squares away from the Safe Rooms in a board draft (`dojo sim --quadrant board/drafts/...`) and compare. The board must still pass `dojo board validate`.
|
||||
- [ ] 6.6 Kiai +2 vs +1: 1,000 games each on two lineups. Watch the Exam pass rate, upset rate and how often a Tecnica turns a fight.
|
||||
- [ ] 6.7 Player counts: 3 / 4 / 6 / 8 players, start 0; plus start 3 at 6 and 8 players. Minutes per count; decide whether 6+ players start on Yellow.
|
||||
- [ ] 6.9 Catch-up candidate A (D38), next in the order (D41; 6.2 suspended, D43): `RulesConfig` switch, Allenamento +2 for a player 2+ belts behind the leader (bot aware of it); compare on the contention, lead and length measures and on the style win rates.
|
||||
- [ ] 6.8 Consolidation: the final configuration at 3 / 4 / 6 / 8 players with all lineups, 2,000 games each; every target checked; `RULES.md` v0.8 with the decisions; open items moved to Phase 7.
|
||||
- [x] 6.10 Simulation speed (before 6.3; D46: games unchanged). Profile: ~95% of the time was listing moves (`movement._extend`, `Board.neighbours`, the outcome keys in `Engine._path_choices`).
|
||||
- [x] 6.10.1 Guard: `agents/scripts/fingerprint_games.py` hashes every event of 210 games (mixed / casual at 4 players, 6 players, Fatica on stop with Kumite in round 1, random bots, 60-round cap); `write` before an engine change, `check` after. A tool for refactoring and speed work, not a test (rule changes are expected to differ). Timings came from a scratch benchmark.
|
||||
- [x] 6.10.2 `Board`: neighbour table, squares per code and Safe Rooms computed once (`cached_property`); neighbour order N, E, S, W kept (the random and casual bots pick by position in the offered list). `Engine`: Fatica squares as a set (`_tolls`), opponents' squares looked up once per move (`_fightable_squares`), and paths crossing no opponent keyed by their Fatica count with C-level set operations.
|
||||
- [x] 6.10.3 `movement.walks`: walks that exclude nothing but the start are cached per (board, start, steps, Dojo open) with `functools.cache` (same pattern as `bots.dojo_distance`); callers get a fresh list. ~3,200 entries per process, ~80 MB peak.
|
||||
- [x] 6.10.4 Parallel scaling: 8 processes running at once each take ~1.6x their solo time on this machine (8 real cores, clock and cache sharing), so ~5x on 8 workers is the hardware limit; no pool change. 16 workers (hyperthreads) add ~12%.
|
||||
- Result: 100 games on one process 8–12 s → 1.4–1.7 s; mixed + balanced + casual at 1,000 games on 16 workers 57 s → 8 s. All 210 fingerprinted games identical; the v0.8 baseline run prints the same metrics.
|
||||
- [ ] 6.9 Catch-up candidate A — **skipped (D44)**. Was: `RulesConfig` switch, Allenamento +2 for a player 2+ belts behind the leader (bot aware of it); compare on the contention, lead and length measures and on the style win rates.
|
||||
- [ ] 6.8 Consolidation: the final configuration at 3 / 4 / 6 / 8 players with all lineups except `random` (D45), 2,000 games each; every target checked; `RULES.md` updated to the next version with the decisions; open items moved to Phase 7.
|
||||
|
||||
### Phase 7 — Finalisation (later)
|
||||
|
||||
@@ -306,9 +315,9 @@ Deliverables:
|
||||
### Risks
|
||||
|
||||
- Movement enumeration: self-avoiding walks of length up to 12 can explode in open rooms. Keep rooms small; prune by outcome (destination + ordered opponents crossed + Dojo entry).
|
||||
- Engine speed ~1,400 turns/s (random and heuristic bots, real board). If Phase 5 needs more: precompute neighbour lists in `Board`.
|
||||
- Engine speed: after 6.10, ~65 games/s per process (heuristic and casual bots). Further gains would need changing the move enumeration itself (e.g. grouping walks by outcome), not attempted.
|
||||
- Simulation conclusions depend on bot quality; compare several profiles before trusting a result.
|
||||
- Board draft 1 puts 4 Allenamento in each 3x3 room next to the Safe Room: possible "home farming" (see the Phase 2 review). Measured (Phase 5 baseline): square income is +0.38/turn in the own quadrant vs +0.14/turn elsewhere; 6.5 decides whether that is a problem. Board draft 2 (D42) moved them to the Dojo ring: 62–67%.
|
||||
- Home farming: board draft 2 (D42) moved the Allenamento onto the Dojo ring; the own-quadrant share of square income is 62–67% (draft 1: 74%), above the 60% aim because players cross their own quadrant for most of the game. Accepted with draft 2 (6.5).
|
||||
|
||||
## External documentation
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user