code wiki / _hdl_build / nx_user_sim_games.nx
nx_user_sim_games.nx source
↩ module page · 142 lines · 6438 B
1// nx_user_sim_games.nx -- CHAOS-USER simulator for the C-stage games
2// (operator: "playtesting isnt just api but actually mimics real users").
3// Drives nx_tictactoe and nx_checkers through the EXACT functions the
4// browser click-handlers call (nx_ttt_try_apply / nx_ttt_pick /
5// nx_chk_apply_*) -- the WASM export surface IS the UI's entry surface --
6// but in REAL-USER patterns, not happy-path API order:
7// * misclicks: occupied cells, out-of-range indices, double-clicks
8// * impatient clicking after the game has ended
9// * full seeded games to termination against the AI
10// Asserts the invariants a real player experiences: illegal input is a
11// visible no-op (state hash unchanged), games terminate, the perfect AI
12// never loses, a finished board ignores clicks.
13//
14// This is the P0 guided-bot seed from the honesty roadmap. Honest scope:
15// covers the export surface; true DOM-event/pixel mimicry needs the SS22.5
16// sovereign browser-test-runner (PM plan). Catches the W/S-inversion CLASS
17// at the state layer: user input -> wrong state transition.
18//
19// Exit: 0 all green (prints USER-SIM ALL-PASS); 1-6 = failing invariant.
20// license_tier: ORIGINAL
21import "nx_syscalls.nx"
22import "nx_tier.nx"
23import "nx_tictactoe.nx"
24import "nx_checkers.nx"
25import "nx_prng.nx"
26const K_MAGIC_20260610: i64 = 20260610
27
28func us_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
29
30func main() -> nx_int {
31 var prng_state: i64 = 0
32 let rng: *i64 = (&prng_state) as *i64
33 nx_prng_init(rng, K_MAGIC_20260610)
34
35 // ===== tic-tac-toe: 20 chaos games, user(X, random clicker) vs perfect AI(O)
36 var g: i64 = 0
37 while g < 20 {
38 let s: *i64 = nx_ttt_new(NX_TTT_X)
39 var applied: i64 = 0
40 var spins: i64 = 0
41 while nx_ttt_outcome(s) == NX_TTT_ONGOING {
42 spins = spins + 1
43 if spins > 500 { return 2 } // game must terminate
44 if nx_ttt_turn(s) == NX_TTT_X {
45 // chaos click: -2..10 (misclicks included)
46 let cell: i64 = nx_prng_range(rng, 13) - 2
47 let h0: i64 = nx_ttt_state_hash(s)
48 let r: i64 = nx_ttt_try_apply(s, cell)
49 if r == 0 {
50 if nx_ttt_state_hash(s) != h0 { return 1 } // rejected click mutated state
51 }
52 if r == 1 { applied = applied + 1 }
53 // impatient double-click on the same cell
54 if r == 1 {
55 let h1: i64 = nx_ttt_state_hash(s)
56 if nx_ttt_try_apply(s, cell) != 0 { return 1 }
57 if nx_ttt_state_hash(s) != h1 { return 1 }
58 }
59 } else {
60 let mv: nx_int = nx_ttt_pick_perfect(s, NX_TTT_O)
61 if mv < 0 { return 2 }
62 nx_ttt_try_apply(s, mv)
63 applied = applied + 1
64 }
65 }
66 if applied > 9 { return 2 }
67 // perfect AI must never LOSE to a random clicker
68 if nx_ttt_outcome(s) == NX_TTT_WIN {
69 if nx_ttt_winner(s) == NX_TTT_X { return 3 }
70 }
71 // finished board ignores further clicks
72 let hend: i64 = nx_ttt_state_hash(s)
73 var k: i64 = 0
74 while k < 5 {
75 let c2: i64 = nx_prng_range(rng, 9)
76 if nx_ttt_try_apply(s, c2) != 0 { return 4 }
77 k = k + 1
78 }
79 if nx_ttt_state_hash(s) != hend { return 4 }
80 g = g + 1
81 }
82
83 // ===== checkers: 10 seeded games, BOTH sides random users picking from
84 // what the UI would highlight (legal jumps first per mandatory-capture,
85 // else legal moves), plus random illegal pokes every turn.
86 let mvbuf: *i64 = sys_mmap(8 * 4 * 64) as *i64
87 var cg: i64 = 0
88 while cg < 10 {
89 let cs: *i64 = nx_chk_new()
90 var plies: i64 = 0
91 while plies < 200 {
92 if nx_chk_outcome(cs) != NX_CHK_ONGOING { plies = 200 }
93 if plies < 200 {
94 let side: i64 = nx_chk_turn(cs)
95 // random illegal poke: apply a random simple move; if rejected,
96 // board must be unchanged (compare piece count + turn)
97 let n_before: i64 = nx_chk_count_side(cs, side)
98 let t_before: i64 = nx_chk_turn(cs)
99 let fr: i64 = nx_prng_range(rng, 8)
100 let fc: i64 = nx_prng_range(rng, 8)
101 let pr: i64 = nx_prng_range(rng, 8)
102 let pc: i64 = nx_prng_range(rng, 8)
103 let pk: i64 = nx_chk_apply_simple_move(cs, fr, fc, pr, pc)
104 if pk == 0 {
105 if nx_chk_count_side(cs, side) != n_before { return 5 }
106 }
107 if pk == 1 { plies = plies + 1 }
108 if pk == 0 {
109 if nx_chk_turn(cs) != t_before { return 5 }
110 // legitimate user move: jumps first (mandatory), else simple
111 var nmv: i64 = nx_chk_legal_jumps(cs, side, mvbuf)
112 var is_jump: i64 = 1
113 if nmv == 0 {
114 nmv = nx_chk_legal_moves(cs, side, mvbuf)
115 is_jump = 0
116 }
117 if nmv == 0 { plies = 200 } // no moves: outcome decided
118 if nmv > 0 {
119 let pick: i64 = nx_prng_range(rng, nmv)
120 let q: i64 = pick * 4
121 var ar: i64 = 0
122 if is_jump == 1 { ar = nx_chk_apply_jump(cs, mvbuf[q], mvbuf[q+1], mvbuf[q+2], mvbuf[q+3]) }
123 if is_jump == 0 { ar = nx_chk_apply_simple_move(cs, mvbuf[q], mvbuf[q+1], mvbuf[q+2], mvbuf[q+3]) }
124 if ar != 1 { return 5 } // UI-highlighted move must apply
125 plies = plies + 1
126 }
127 }
128 }
129 }
130 // post-game (or cap): a finished game must reject any further move
131 if nx_chk_outcome(cs) != NX_CHK_ONGOING {
132 let nb: i64 = nx_chk_count_side(cs, NX_CHK_RED) + nx_chk_count_side(cs, NX_CHK_BLACK)
133 nx_chk_apply_simple_move(cs, 5, 0, 4, 1)
134 let na: i64 = nx_chk_count_side(cs, NX_CHK_RED) + nx_chk_count_side(cs, NX_CHK_BLACK)
135 if na != nb { return 6 }
136 }
137 cg = cg + 1
138 }
139
140 us_puts("USER-SIM ALL-PASS\n" as *u8)
141 return 0
142}