nx_tictactoe_test.nx source
↩ module page · 173 lines · 7596 B
1// nx_tictactoe_test.nx -- C0 substrate smoke.
2//
3// 12 deterministic assertions covering:
4// 1. new game has X to move + outcome ongoing
5// 2. 9 legal moves on a fresh board
6// 3. apply X to cell 4 -> board[4]=X, turn=O, 8 legal moves left
7// 4. illegal-move (occupied cell) is no-op
8// 5. illegal-move (out-of-range) is no-op
9// 6. horizontal-row win is detected (top row XXX)
10// 7. diagonal win is detected (0-4-8 XXX)
11// 8. draw is detected after 9 non-winning moves
12// 9. state-hash is deterministic for same state
13// 10. perfect-vs-perfect always ends in draw (game-theory invariant)
14// 11. perfect-vs-random never loses (perfect's score never negative)
15// 12. evaluate updates only the OUTCOME and WINNER cells (does not mutate board)
16//
17// Each failed assertion exits with a distinct non-zero code so the smoke
18// harness identifies which check fired.
19//
20// Composes [[feedback-bug-class-prevention-additive-not-restrictive]] +
21// ยง22 bug-class table entry "move-applies-to-wrong-cell" (the tic-tac-toe
22// equivalent of the W/S inversion class).
23
24import "nx_syscalls.nx"
25import "nx_tier.nx"
26import "nx_tictactoe.nx"
27import "nx_prng.nx"
28
29func main() -> nx_int {
30 // ===== Assertion 1: new game state ==================================
31 let s1: *i64 = nx_ttt_new(NX_TTT_X)
32 if nx_ttt_turn(s1) != NX_TTT_X { return __syscall(93, 1, 0, 0, 0, 0, 0) }
33 if nx_ttt_outcome(s1) != NX_TTT_ONGOING { return __syscall(93, 1, 0, 0, 0, 0, 0) }
34 if nx_ttt_winner(s1) != 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
35
36 // ===== Assertion 2: 9 legal moves on fresh board ====================
37 if nx_ttt_legal_count(s1) != 9 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
38
39 // ===== Assertion 3: apply X to cell 4 ===============================
40 if nx_ttt_try_apply(s1, 4) != 1 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
41 if nx_ttt_cell(s1, 4) != NX_TTT_X { return __syscall(93, 3, 0, 0, 0, 0, 0) }
42 if nx_ttt_turn(s1) != NX_TTT_O { return __syscall(93, 3, 0, 0, 0, 0, 0) }
43 if nx_ttt_legal_count(s1) != 8 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
44
45 // ===== Assertion 4: illegal move (occupied cell) is no-op ===========
46 let hash_before: i64 = nx_ttt_state_hash(s1)
47 if nx_ttt_try_apply(s1, 4) != 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
48 if nx_ttt_state_hash(s1) != hash_before { return __syscall(93, 4, 0, 0, 0, 0, 0) }
49
50 // ===== Assertion 5: illegal move (out-of-range) is no-op ============
51 if nx_ttt_try_apply(s1, -1) != 0 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
52 if nx_ttt_try_apply(s1, 9) != 0 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
53 if nx_ttt_state_hash(s1) != hash_before { return __syscall(93, 5, 0, 0, 0, 0, 0) }
54
55 // ===== Assertion 6: horizontal-row win detected =====================
56 // Reset and play X: 0,1,2 / O: 3,4 (X wins top row)
57 let s2: *i64 = nx_ttt_new(NX_TTT_X)
58 nx_ttt_try_apply(s2, 0) // X@0
59 nx_ttt_try_apply(s2, 3) // O@3
60 nx_ttt_try_apply(s2, 1) // X@1
61 nx_ttt_try_apply(s2, 4) // O@4
62 nx_ttt_try_apply(s2, 2) // X@2 wins
63 if nx_ttt_outcome(s2) != NX_TTT_WIN { return __syscall(93, 6, 0, 0, 0, 0, 0) }
64 if nx_ttt_winner(s2) != NX_TTT_X { return __syscall(93, 6, 0, 0, 0, 0, 0) }
65
66 // ===== Assertion 7: diagonal win detected ===========================
67 let s3: *i64 = nx_ttt_new(NX_TTT_X)
68 nx_ttt_try_apply(s3, 0) // X@0
69 nx_ttt_try_apply(s3, 1) // O@1
70 nx_ttt_try_apply(s3, 4) // X@4
71 nx_ttt_try_apply(s3, 2) // O@2
72 nx_ttt_try_apply(s3, 8) // X@8 wins diagonal
73 if nx_ttt_outcome(s3) != NX_TTT_WIN { return __syscall(93, 7, 0, 0, 0, 0, 0) }
74 if nx_ttt_winner(s3) != NX_TTT_X { return __syscall(93, 7, 0, 0, 0, 0, 0) }
75
76 // ===== Assertion 8: draw detected ===================================
77 // X O X
78 // X O O
79 // O X X (moves: 0 1 2 4 3 6 7 8 5)
80 let s4: *i64 = nx_ttt_new(NX_TTT_X)
81 nx_ttt_try_apply(s4, 0)
82 nx_ttt_try_apply(s4, 1)
83 nx_ttt_try_apply(s4, 2)
84 nx_ttt_try_apply(s4, 4)
85 nx_ttt_try_apply(s4, 3)
86 nx_ttt_try_apply(s4, 6)
87 nx_ttt_try_apply(s4, 7)
88 nx_ttt_try_apply(s4, 8)
89 nx_ttt_try_apply(s4, 5)
90 if nx_ttt_outcome(s4) != NX_TTT_DRAW { return __syscall(93, 8, 0, 0, 0, 0, 0) }
91
92 // ===== Assertion 9: state-hash is deterministic =====================
93 let s5: *i64 = nx_ttt_new(NX_TTT_X)
94 nx_ttt_try_apply(s5, 4)
95 nx_ttt_try_apply(s5, 0)
96 nx_ttt_try_apply(s5, 8)
97 let h_a: i64 = nx_ttt_state_hash(s5)
98 let h_b: i64 = nx_ttt_state_hash(s5)
99 if h_a != h_b { return __syscall(93, 9, 0, 0, 0, 0, 0) }
100 // Different state -> different hash (overwhelmingly likely)
101 nx_ttt_try_apply(s5, 1)
102 let h_c: i64 = nx_ttt_state_hash(s5)
103 if h_a == h_c { return __syscall(93, 9, 0, 0, 0, 0, 0) }
104
105 // ===== Assertion 10: perfect-vs-perfect always draws ================
106 // Two minimax agents -- game theory: optimal play -> draw.
107 let s6: *i64 = nx_ttt_new(NX_TTT_X)
108 var moves_played: nx_int = 0
109 while nx_ttt_outcome(s6) == NX_TTT_ONGOING {
110 let mv: nx_int = nx_ttt_pick_perfect(s6, nx_ttt_turn(s6))
111 if mv < 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
112 nx_ttt_try_apply(s6, mv)
113 moves_played = moves_played + 1
114 if moves_played > 9 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
115 }
116 if nx_ttt_outcome(s6) != NX_TTT_DRAW { return __syscall(93, 10, 0, 0, 0, 0, 0) }
117
118 // ===== Assertion 11: perfect-vs-random -- perfect never loses =======
119 // 5 trials. Perfect (X) vs random (O). X must win or draw.
120 var prng_state: i64 = 0
121 let prng_ptr: *i64 = (&prng_state) as *i64
122 nx_prng_init(prng_ptr, 42 as i64)
123 var trial: nx_int = 0
124 while trial < 5 {
125 let s7: *i64 = nx_ttt_new(NX_TTT_X)
126 while nx_ttt_outcome(s7) == NX_TTT_ONGOING {
127 var mv: nx_int = -1
128 if nx_ttt_turn(s7) == NX_TTT_X {
129 mv = nx_ttt_pick_perfect(s7, NX_TTT_X)
130 } else {
131 mv = nx_ttt_pick_easy(s7, prng_ptr)
132 }
133 if mv < 0 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
134 nx_ttt_try_apply(s7, mv)
135 }
136 if nx_ttt_outcome(s7) == NX_TTT_WIN {
137 if nx_ttt_winner(s7) != NX_TTT_X { return __syscall(93, 11, 0, 0, 0, 0, 0) }
138 }
139 // DRAW is acceptable; LOSS (X is winner -> not X) is the failure.
140 trial = trial + 1
141 }
142
143 // ===== Assertion 12: evaluate does not mutate board cells ===========
144 let s8: *i64 = nx_ttt_new(NX_TTT_X)
145 nx_ttt_try_apply(s8, 0)
146 nx_ttt_try_apply(s8, 4)
147 nx_ttt_try_apply(s8, 1)
148 // Snapshot board cells
149 var snapshot: nx_int = 0
150 snapshot = snapshot * 3 + s8[0]
151 snapshot = snapshot * 3 + s8[1]
152 snapshot = snapshot * 3 + s8[2]
153 snapshot = snapshot * 3 + s8[3]
154 snapshot = snapshot * 3 + s8[4]
155 snapshot = snapshot * 3 + s8[5]
156 snapshot = snapshot * 3 + s8[6]
157 snapshot = snapshot * 3 + s8[7]
158 snapshot = snapshot * 3 + s8[8]
159 nx_ttt_evaluate(s8)
160 var after: nx_int = 0
161 after = after * 3 + s8[0]
162 after = after * 3 + s8[1]
163 after = after * 3 + s8[2]
164 after = after * 3 + s8[3]
165 after = after * 3 + s8[4]
166 after = after * 3 + s8[5]
167 after = after * 3 + s8[6]
168 after = after * 3 + s8[7]
169 after = after * 3 + s8[8]
170 if after != snapshot { return __syscall(93, 12, 0, 0, 0, 0, 0) }
171
172 return 0
173}