Spaces:
Sleeping
Sleeping
| """Validate togyz/rules.py against the 9Q engine's shortest-game fixture. | |
| Replays tests/fixtures/shortest_candidate_replay.tsv move by move and asserts | |
| that notation, kazans, side totals, tuzdyks, and the winner all match the | |
| C++ engine's recorded state after every halfmove. | |
| python tests/test_rules.py | |
| """ | |
| import csv | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from togyz.rules import Game | |
| FIXTURE = Path(__file__).parent / "fixtures" / "shortest_candidate_replay.tsv" | |
| def main() -> None: | |
| with open(FIXTURE, newline="") as f: | |
| lines = f.read().splitlines() | |
| rows = list(csv.DictReader(lines[2:], delimiter="\t")) # skip 2 meta lines | |
| game = Game() | |
| for row in rows: | |
| expected = row["move"] | |
| player = 0 if row["player"] == "P1" else 1 | |
| assert game.to_play == player, f"halfmove {row['halfmove']}: wrong side to move" | |
| legal = {m.notation for m in game.legal_moves()} | |
| assert expected in legal, f"halfmove {row['halfmove']}: {expected} not in {legal}" | |
| played = game.play_notation(expected) | |
| assert played.notation == expected | |
| state = (game.kazans[0], game.kazans[1], *game.side_totals(), | |
| game.tuzdyks[0], game.tuzdyks[1]) | |
| want = tuple(int(row[k]) for k in | |
| ("p1_kazan", "p2_kazan", "p1_side", "p2_side", "p1_tuzdyk", "p2_tuzdyk")) | |
| assert state == want, ( | |
| f"halfmove {row['halfmove']} ({expected}): state {state} != fixture {want}" | |
| ) | |
| if row["winner"] != "ongoing": | |
| assert game.is_over, f"halfmove {row['halfmove']}: game should be over" | |
| expected_winner = {"P1": 0, "P2": 1, "draw": -1}[row["winner"]] | |
| assert game.winner() == expected_winner | |
| print(f"OK: replayed {len(rows)} halfmoves, all states match the 9Q fixture") | |
| # extra sanity: fresh game has exactly 9 legal moves, all distinct notations | |
| fresh = Game() | |
| moves = fresh.legal_moves() | |
| assert len(moves) == 9 and len({m.notation for m in moves}) == 9 | |
| print("OK: initial position has 9 distinct legal moves") | |
| # tuzdyk scenario (not covered by the fixture): P1 sows 3 stones from | |
| # hole 9 -> last stone lands in P2's hole 2 (pit 10) making it 3 -> "92x" | |
| g = Game() | |
| g.pits = [9, 9, 9, 9, 9, 9, 9, 9, 3] + [2, 2, 9, 9, 9, 9, 9, 9, 9] | |
| move = g.play_notation("92x") | |
| assert move.makes_tuzdyk and g.tuzdyks[0] == 1 and g.kazans[0] == 3 | |
| assert g.pits[10] == 0 | |
| # P2 may not move from their own hole 2 (it is P1's tuzdyk now) | |
| assert 1 not in g.legal_actions() | |
| # P2 sows 3 stones from their hole 1 (pit 9): one falls into the tuzdyk | |
| # and must be collected by its owner P1 | |
| g.play_notation("13") | |
| assert g.kazans[0] == 3 + 1, "stone sown into tuzdyk must go to owner" | |
| assert g.pits[10] == 0 | |
| print("OK: tuzdyk creation, blocking, and collection behave correctly") | |
| # BOTH players may create one tuzdyk each (as on the real scoresheet: | |
| # 47x by White then 91x by Black) - but never a second one, never in | |
| # hole 9, and never mirroring the opponent's tuzdyk column. | |
| g = Game() | |
| g.tuzdyks = [4, -1] # White already owns a tuzdyk (Black's hole 5) | |
| g.to_play = 1 | |
| g.pits = [9, 9, 2, 9, 9, 9, 9, 9, 9] + [9] * 9 | |
| # Black hole 4 (pit 12), 9 stones: lands (12+9-1)%18 = pit 2, making 3 | |
| move = g.play_notation("43x") | |
| assert move.makes_tuzdyk and g.tuzdyks == [4, 2] | |
| # a second tuzdyk for the same player must be impossible anywhere | |
| g.to_play = 1 | |
| assert not any(m.makes_tuzdyk for m in g.legal_moves()) | |
| print("OK: both players can own one tuzdyk; no second tuzdyk possible") | |
| # mirror restriction: creation blocked where the opponent's tuzdyk sits | |
| g = Game() | |
| g.tuzdyks = [-1, 2] # Black owns White's hole 3 (column index 2) | |
| g.pits = [9] * 18 | |
| g.pits[8] = 4 # White hole 9: lands (8+4-1)%18 = pit 11 (Black col 2) | |
| g.pits[11] = 2 # becomes 3 on landing - but mirrored, so no tuzdyk | |
| move = g.play_notation("93") | |
| assert not move.makes_tuzdyk and g.tuzdyks[0] == -1 | |
| # hole 9 restriction: landing makes 3 in opponent hole 9 -> no tuzdyk | |
| g = Game() | |
| g.pits = [9] * 18 | |
| g.pits[8] = 10 # White hole 9: lands (8+10-1)%18 = pit 17 (Black hole 9) | |
| g.pits[17] = 2 # becomes 3 on landing | |
| move = g.play_notation("99") | |
| assert not move.makes_tuzdyk and g.tuzdyks[0] == -1, "hole 9 must never be tuzdyk" | |
| print("OK: mirror and hole-9 tuzdyk restrictions enforced") | |
| if __name__ == "__main__": | |
| main() | |