File size: 4,587 Bytes
20d7fde
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
"""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()