nx_checkers.nx source
↩ module page · 832 lines · 32885 B
1// nx_checkers.nx -- C1 of the bootstrap-to-generative trajectory.
2//
3// Standard 8x8 American checkers (English draughts). Pieces on dark
4// squares only; 12 pieces per side; men move forward diagonally, kings
5// move both directions. Captures are jumps over an adjacent opponent
6// to an empty square; multi-captures chain. Win when opponent has no
7// legal moves (no pieces left or all blocked).
8//
9// This file (C1a) ships:
10// - Piece + outcome sealed-enum constants
11// - State block layout (8x8 board + turn + outcome + force-continue)
12// - Allocation + standard starting position
13// - Accessors (cell, turn, outcome, force_continue)
14// - Coordinate helpers (is_dark_square, in_bounds)
15//
16// Deferred to C1b+:
17// - Legal-move generation (simple moves + jumps + mandatory captures)
18// - Multi-capture chaining
19// - Kinging on reaching far row
20// - Win-condition evaluation
21// - AI tiers (easy random / medium 1-ply / minimax-depth-N)
22// - HTML UI + WASM delivery + playwright audit
23//
24// genealogy_id: nx_checkers_v1_2026_05_19
25// lineage_id: abstract_classic_8x8_game_state_machine
26// license: operator-as-sole-author (per §23.1 open question)
27// complexity: O(1) accessors; O(8) legal-move gen per piece; AI depth-bounded
28
29// nx_safety_envelope:
30// intended_use: c1_bootstrap_state_machine
31// sil_target: SIL1
32// evidence: [game_rules_deterministic + initial_position_canonical]
33// verdict: NOT_YET_EVALUATED
34
35import "nx_syscalls.nx"
36import "nx_tier.nx"
37import "nx_prng.nx"
38
39// ===== AI difficulty tiers ============================================
40const NX_CHK_AI_EASY: i64 = 0 // random legal action
41const NX_CHK_AI_MEDIUM: i64 = 1 // (deferred) 1-ply heuristic
42const NX_CHK_AI_HARD: i64 = 2 // (deferred) minimax depth-N alpha-beta
43
44// ===== Piece type values ==============================================
45
46const NX_CHK_EMPTY: i64 = 0
47const NX_CHK_RED_MAN: i64 = 1
48const NX_CHK_RED_KING: i64 = 2
49const NX_CHK_BLACK_MAN: i64 = 3
50const NX_CHK_BLACK_KING: i64 = 4
51
52// ===== Sides ==========================================================
53// Red moves "up" (decreasing row), starts on rows 5-7.
54// Black moves "down" (increasing row), starts on rows 0-2.
55
56const NX_CHK_RED: i64 = 1
57const NX_CHK_BLACK: i64 = 2
58
59// ===== Outcome kinds ==================================================
60
61const NX_CHK_ONGOING: i64 = 0
62const NX_CHK_WIN_RED: i64 = 1
63const NX_CHK_WIN_BLACK: i64 = 2
64const NX_CHK_DRAW: i64 = 3
65
66// ===== State layout ===================================================
67//
68// 64 board cells (row-major, [row][col] -> [row*8+col]) + 4 header fields.
69//
70// [0..63] board[8][8] -- piece value per square
71// [64] turn (NX_CHK_RED or NX_CHK_BLACK)
72// [65] outcome (NX_CHK_ONGOING / WIN_RED / WIN_BLACK / DRAW)
73// [66] force_continue -- square the side must continue capturing from,
74// or -1 if no forced continuation (multi-jump state)
75// [67] plies played (move counter; useful for 40-move-no-capture draw rule)
76
77const NX_CHK_OFF_BOARD: i64 = 0
78const NX_CHK_OFF_TURN: i64 = 64
79const NX_CHK_OFF_OUTCOME: i64 = 65
80const NX_CHK_OFF_FORCE_CONTINUE: i64 = 66
81const NX_CHK_OFF_PLIES: i64 = 67
82const NX_CHK_STATE_CELLS: i64 = 68
83
84// ===== Coordinate helpers =============================================
85//
86// A square is "dark" when (row + col) is odd. Pieces only ever occupy
87// dark squares; light squares always hold NX_CHK_EMPTY.
88
89func nx_chk_in_bounds(row: i64, col: i64) -> i64 {
90 if row < 0 { return 0 }
91 if row > 7 { return 0 }
92 if col < 0 { return 0 }
93 if col > 7 { return 0 }
94 return 1
95}
96
97func nx_chk_is_dark(row: i64, col: i64) -> i64 {
98 let r: i64 = row & 1
99 let c: i64 = col & 1
100 let s: i64 = r + c
101 if s == 1 { return 1 }
102 return 0
103}
104
105func nx_chk_sq(row: i64, col: i64) -> i64 {
106 return row * 8 + col
107}
108
109// ===== Allocation + initial position ==================================
110
111func nx_chk_new() -> *i64 {
112 let s: *i64 = (sys_mmap(NX_CHK_STATE_CELLS * 8)) as *i64
113 // Zero-init the board.
114 var i: i64 = 0
115 while i < NX_CHK_STATE_CELLS {
116 s[i] = 0
117 i = i + 1
118 }
119 // Place 12 black men on dark squares of rows 0, 1, 2 (top of board).
120 var row: i64 = 0
121 while row < 3 {
122 var col: i64 = 0
123 while col < 8 {
124 if nx_chk_is_dark(row, col) == 1 {
125 s[nx_chk_sq(row, col)] = NX_CHK_BLACK_MAN
126 }
127 col = col + 1
128 }
129 row = row + 1
130 }
131 // Place 12 red men on dark squares of rows 5, 6, 7 (bottom of board).
132 row = 5
133 while row < 8 {
134 var col: i64 = 0
135 while col < 8 {
136 if nx_chk_is_dark(row, col) == 1 {
137 s[nx_chk_sq(row, col)] = NX_CHK_RED_MAN
138 }
139 col = col + 1
140 }
141 row = row + 1
142 }
143 // Red moves first per standard rules.
144 s[NX_CHK_OFF_TURN] = NX_CHK_RED
145 s[NX_CHK_OFF_OUTCOME] = NX_CHK_ONGOING
146 s[NX_CHK_OFF_FORCE_CONTINUE] = -1
147 s[NX_CHK_OFF_PLIES] = 0
148 return s
149}
150
151// ===== Accessors ======================================================
152
153func nx_chk_cell(s: *i64, row: i64, col: i64) -> i64 {
154 if nx_chk_in_bounds(row, col) == 0 { return -1 }
155 return s[nx_chk_sq(row, col)]
156}
157
158func nx_chk_cell_by_sq(s: *i64, sq: i64) -> i64 {
159 if sq < 0 { return -1 }
160 if sq > 63 { return -1 }
161 return s[sq]
162}
163
164func nx_chk_turn(s: *i64) -> i64 {
165 return s[NX_CHK_OFF_TURN]
166}
167
168func nx_chk_outcome(s: *i64) -> i64 {
169 return s[NX_CHK_OFF_OUTCOME]
170}
171
172func nx_chk_force_continue(s: *i64) -> i64 {
173 return s[NX_CHK_OFF_FORCE_CONTINUE]
174}
175
176func nx_chk_plies(s: *i64) -> i64 {
177 return s[NX_CHK_OFF_PLIES]
178}
179
180// ===== Piece classification ==========================================
181
182func nx_chk_is_red(piece: i64) -> i64 {
183 if piece == NX_CHK_RED_MAN { return 1 }
184 if piece == NX_CHK_RED_KING { return 1 }
185 return 0
186}
187
188func nx_chk_is_black(piece: i64) -> i64 {
189 if piece == NX_CHK_BLACK_MAN { return 1 }
190 if piece == NX_CHK_BLACK_KING { return 1 }
191 return 0
192}
193
194func nx_chk_is_king(piece: i64) -> i64 {
195 if piece == NX_CHK_RED_KING { return 1 }
196 if piece == NX_CHK_BLACK_KING { return 1 }
197 return 0
198}
199
200// Returns NX_CHK_RED, NX_CHK_BLACK, or 0 (empty/error).
201func nx_chk_side_of(piece: i64) -> i64 {
202 if nx_chk_is_red(piece) == 1 { return NX_CHK_RED }
203 if nx_chk_is_black(piece) == 1 { return NX_CHK_BLACK }
204 return 0
205}
206
207// Other side (for turn flipping).
208func nx_chk_other(side: i64) -> i64 {
209 if side == NX_CHK_RED { return NX_CHK_BLACK }
210 return NX_CHK_RED
211}
212// ===== Piece counts (for win-check + heuristic eval later) ===========
213
214func nx_chk_count_side(s: *i64, side: i64) -> i64 {
215 var n: i64 = 0
216 var i: i64 = 0
217 while i < 64 {
218 let p: i64 = s[i]
219 if nx_chk_side_of(p) == side { n = n + 1 }
220 i = i + 1
221 }
222 return n
223}
224// ===== Legal-move enumeration (simple moves; captures NOT yet) ========
225//
226// Enumerates every legal simple (non-capture) move for `side`. Writes
227// quadruples (from_row, from_col, to_row, to_col) into out_buf as flat
228// i64 cells; returns the count of moves emitted.
229//
230// Layout in out_buf:
231// out_buf[0..3] = move 0 (from_row, from_col, to_row, to_col)
232// out_buf[4..7] = move 1
233// ...
234//
235// Caller is responsible for buffer sizing. Maximum possible simple
236// moves: 12 pieces × 4 diagonals each = 48 quadruples = 192 i64 cells.
237//
238// NOTE: this counts ONLY simple moves. C1c will add jump enumeration
239// + mandatory-capture rule (if jumps exist, simple moves are illegal).
240//
241// Test invariants:
242// - From the standard initial position, each side has exactly 7
243// legal simple moves (only the third-rank-from-side can move;
244// each unblocked square has 1-2 diagonal targets in bounds).
245// Legal-jump enumeration. Writes quadruples (from_row, from_col, to_row,
246// to_col) describing every legal single-jump for `side`. Returns count.
247// Multi-jump chaining still NOT applied here -- this only lists the FIRST
248// jump in any chain. C1c-4 will add chained-jump enumeration.
249func nx_chk_legal_jumps(s: *i64, side: i64, out_buf: *i64) -> i64 {
250 if s[NX_CHK_OFF_OUTCOME] != NX_CHK_ONGOING { return 0 }
251 var count: i64 = 0
252
253 var forward_dr: i64 = 1
254 if side == NX_CHK_RED { forward_dr = -1 }
255
256 // C1c-4: during a forced multi-jump chain, only the chain piece may jump.
257 let fc: i64 = s[NX_CHK_OFF_FORCE_CONTINUE]
258
259 var row: i64 = 0
260 while row < 8 {
261 var col: i64 = 0
262 while col < 8 {
263 if nx_chk_is_dark(row, col) == 1 {
264 let piece: i64 = s[nx_chk_sq(row, col)]
265 var allowed: i64 = 1
266 if fc >= 0 {
267 if nx_chk_sq(row, col) != fc { allowed = 0 }
268 }
269 if nx_chk_side_of(piece) == side {
270 if allowed == 1 {
271 let is_king_piece: i64 = nx_chk_is_king(piece)
272
273 // For each of up to 4 diagonals (2 forward + 2 backward
274 // for kings), check the 2-step-away square + midpoint.
275 // Inline all 4 directions to avoid loop+array complexity.
276
277 // Forward-left (dr_sign, dc_sign) = (forward_dr, -1)
278 let f_mid_r: i64 = row + forward_dr
279 let fl_mid_c: i64 = col - 1
280 let f_to_r: i64 = row + 2 * forward_dr
281 let fl_to_c: i64 = col - 2
282 if nx_chk_in_bounds(f_to_r, fl_to_c) == 1 {
283 if nx_chk_is_dark(f_to_r, fl_to_c) == 1 {
284 if s[nx_chk_sq(f_to_r, fl_to_c)] == NX_CHK_EMPTY {
285 let mp: i64 = s[nx_chk_sq(f_mid_r, fl_mid_c)]
286 if nx_chk_side_of(mp) == nx_chk_other(side) {
287 out_buf[count * 4 + 0] = row
288 out_buf[count * 4 + 1] = col
289 out_buf[count * 4 + 2] = f_to_r
290 out_buf[count * 4 + 3] = fl_to_c
291 count = count + 1
292 }
293 }
294 }
295 }
296 // Forward-right
297 let fr_mid_c: i64 = col + 1
298 let fr_to_c: i64 = col + 2
299 if nx_chk_in_bounds(f_to_r, fr_to_c) == 1 {
300 if nx_chk_is_dark(f_to_r, fr_to_c) == 1 {
301 if s[nx_chk_sq(f_to_r, fr_to_c)] == NX_CHK_EMPTY {
302 let mp: i64 = s[nx_chk_sq(f_mid_r, fr_mid_c)]
303 if nx_chk_side_of(mp) == nx_chk_other(side) {
304 out_buf[count * 4 + 0] = row
305 out_buf[count * 4 + 1] = col
306 out_buf[count * 4 + 2] = f_to_r
307 out_buf[count * 4 + 3] = fr_to_c
308 count = count + 1
309 }
310 }
311 }
312 }
313 if is_king_piece == 1 {
314 // Backward-left
315 let b_mid_r: i64 = row - forward_dr
316 let bl_mid_c: i64 = col - 1
317 let b_to_r: i64 = row - 2 * forward_dr
318 let bl_to_c: i64 = col - 2
319 if nx_chk_in_bounds(b_to_r, bl_to_c) == 1 {
320 if nx_chk_is_dark(b_to_r, bl_to_c) == 1 {
321 if s[nx_chk_sq(b_to_r, bl_to_c)] == NX_CHK_EMPTY {
322 let mp: i64 = s[nx_chk_sq(b_mid_r, bl_mid_c)]
323 if nx_chk_side_of(mp) == nx_chk_other(side) {
324 out_buf[count * 4 + 0] = row
325 out_buf[count * 4 + 1] = col
326 out_buf[count * 4 + 2] = b_to_r
327 out_buf[count * 4 + 3] = bl_to_c
328 count = count + 1
329 }
330 }
331 }
332 }
333 // Backward-right
334 let br_mid_c: i64 = col + 1
335 let br_to_c: i64 = col + 2
336 if nx_chk_in_bounds(b_to_r, br_to_c) == 1 {
337 if nx_chk_is_dark(b_to_r, br_to_c) == 1 {
338 if s[nx_chk_sq(b_to_r, br_to_c)] == NX_CHK_EMPTY {
339 let mp: i64 = s[nx_chk_sq(b_mid_r, br_mid_c)]
340 if nx_chk_side_of(mp) == nx_chk_other(side) {
341 out_buf[count * 4 + 0] = row
342 out_buf[count * 4 + 1] = col
343 out_buf[count * 4 + 2] = b_to_r
344 out_buf[count * 4 + 3] = br_to_c
345 count = count + 1
346 }
347 }
348 }
349 }
350 }
351 }
352 }
353 }
354 col = col + 1
355 }
356 row = row + 1
357 }
358 return count
359}
360
361func nx_chk_legal_moves(s: *i64, side: i64, out_buf: *i64) -> i64 {
362 if s[NX_CHK_OFF_OUTCOME] != NX_CHK_ONGOING { return 0 }
363 // C1c-4: during a forced multi-jump chain, no simple moves are legal.
364 if s[NX_CHK_OFF_FORCE_CONTINUE] >= 0 { return 0 }
365 var count: i64 = 0
366
367 // Forward direction for this side: red = -1 (rows decreasing),
368 // black = +1 (rows increasing).
369 var forward_dr: i64 = 1
370 if side == NX_CHK_RED { forward_dr = -1 }
371
372 var row: i64 = 0
373 while row < 8 {
374 var col: i64 = 0
375 while col < 8 {
376 if nx_chk_is_dark(row, col) == 1 {
377 let piece: i64 = s[nx_chk_sq(row, col)]
378 if nx_chk_side_of(piece) == side {
379 // Determine which diagonals this piece may use.
380 // Men: only the two forward diagonals.
381 // Kings: all four diagonals.
382 let is_king_piece: i64 = nx_chk_is_king(piece)
383
384 // Forward-left
385 let f_r: i64 = row + forward_dr
386 let fl_c: i64 = col - 1
387 if nx_chk_in_bounds(f_r, fl_c) == 1 {
388 if nx_chk_is_dark(f_r, fl_c) == 1 {
389 if s[nx_chk_sq(f_r, fl_c)] == NX_CHK_EMPTY {
390 out_buf[count * 4 + 0] = row
391 out_buf[count * 4 + 1] = col
392 out_buf[count * 4 + 2] = f_r
393 out_buf[count * 4 + 3] = fl_c
394 count = count + 1
395 }
396 }
397 }
398 // Forward-right
399 let fr_c: i64 = col + 1
400 if nx_chk_in_bounds(f_r, fr_c) == 1 {
401 if nx_chk_is_dark(f_r, fr_c) == 1 {
402 if s[nx_chk_sq(f_r, fr_c)] == NX_CHK_EMPTY {
403 out_buf[count * 4 + 0] = row
404 out_buf[count * 4 + 1] = col
405 out_buf[count * 4 + 2] = f_r
406 out_buf[count * 4 + 3] = fr_c
407 count = count + 1
408 }
409 }
410 }
411 // Backward diagonals -- kings only.
412 if is_king_piece == 1 {
413 let b_r: i64 = row - forward_dr
414 let bl_c: i64 = col - 1
415 if nx_chk_in_bounds(b_r, bl_c) == 1 {
416 if nx_chk_is_dark(b_r, bl_c) == 1 {
417 if s[nx_chk_sq(b_r, bl_c)] == NX_CHK_EMPTY {
418 out_buf[count * 4 + 0] = row
419 out_buf[count * 4 + 1] = col
420 out_buf[count * 4 + 2] = b_r
421 out_buf[count * 4 + 3] = bl_c
422 count = count + 1
423 }
424 }
425 }
426 let br_c: i64 = col + 1
427 if nx_chk_in_bounds(b_r, br_c) == 1 {
428 if nx_chk_is_dark(b_r, br_c) == 1 {
429 if s[nx_chk_sq(b_r, br_c)] == NX_CHK_EMPTY {
430 out_buf[count * 4 + 0] = row
431 out_buf[count * 4 + 1] = col
432 out_buf[count * 4 + 2] = b_r
433 out_buf[count * 4 + 3] = br_c
434 count = count + 1
435 }
436 }
437 }
438 }
439 }
440 }
441 col = col + 1
442 }
443 row = row + 1
444 }
445 return count
446}
447// ===== Jumps-exist predicate (for mandatory-capture rule) =============
448//
449// Cheap "does side have any jumps?" check. Used by apply_simple_move to
450// enforce that simple moves are illegal when captures are available.
451// Returns 1 if any jump exists, 0 otherwise.
452// ===== Piece-can-jump-from helper (chain continuation check) ==========
453//
454// Given a square, returns 1 if the piece at that square can make AT LEAST
455// ONE jump (single capture) from its current position. Used by apply_jump
456// to detect multi-jump chain continuation per standard American checkers.
457// Returns 0 if the square is empty / out of bounds / has no available jump.
458func nx_chk_piece_can_jump_from(s: *i64, sq: i64) -> i64 {
459 if sq < 0 { return 0 }
460 if sq > 63 { return 0 }
461 let piece: i64 = s[sq]
462 let side: i64 = nx_chk_side_of(piece)
463 if side == 0 { return 0 }
464 let is_k: i64 = nx_chk_is_king(piece)
465 let row: i64 = sq / 8
466 let col: i64 = sq - row * 8
467 var forward_dr: i64 = 1
468 if side == NX_CHK_RED { forward_dr = -1 }
469 let opp: i64 = nx_chk_other(side)
470
471 // Forward-left
472 let f_mid_r: i64 = row + forward_dr
473 let fl_mid_c: i64 = col - 1
474 let f_to_r: i64 = row + 2 * forward_dr
475 let fl_to_c: i64 = col - 2
476 if nx_chk_in_bounds(f_to_r, fl_to_c) == 1 {
477 if nx_chk_is_dark(f_to_r, fl_to_c) == 1 {
478 if s[nx_chk_sq(f_to_r, fl_to_c)] == NX_CHK_EMPTY {
479 let mp: i64 = s[nx_chk_sq(f_mid_r, fl_mid_c)]
480 if nx_chk_side_of(mp) == opp { return 1 }
481 }
482 }
483 }
484 // Forward-right
485 let fr_mid_c: i64 = col + 1
486 let fr_to_c: i64 = col + 2
487 if nx_chk_in_bounds(f_to_r, fr_to_c) == 1 {
488 if nx_chk_is_dark(f_to_r, fr_to_c) == 1 {
489 if s[nx_chk_sq(f_to_r, fr_to_c)] == NX_CHK_EMPTY {
490 let mp: i64 = s[nx_chk_sq(f_mid_r, fr_mid_c)]
491 if nx_chk_side_of(mp) == opp { return 1 }
492 }
493 }
494 }
495 if is_k == 1 {
496 // Backward-left
497 let b_mid_r: i64 = row - forward_dr
498 let bl_mid_c: i64 = col - 1
499 let b_to_r: i64 = row - 2 * forward_dr
500 let bl_to_c: i64 = col - 2
501 if nx_chk_in_bounds(b_to_r, bl_to_c) == 1 {
502 if nx_chk_is_dark(b_to_r, bl_to_c) == 1 {
503 if s[nx_chk_sq(b_to_r, bl_to_c)] == NX_CHK_EMPTY {
504 let mp: i64 = s[nx_chk_sq(b_mid_r, bl_mid_c)]
505 if nx_chk_side_of(mp) == opp { return 1 }
506 }
507 }
508 }
509 // Backward-right
510 let br_mid_c: i64 = col + 1
511 let br_to_c: i64 = col + 2
512 if nx_chk_in_bounds(b_to_r, br_to_c) == 1 {
513 if nx_chk_is_dark(b_to_r, br_to_c) == 1 {
514 if s[nx_chk_sq(b_to_r, br_to_c)] == NX_CHK_EMPTY {
515 let mp: i64 = s[nx_chk_sq(b_mid_r, br_mid_c)]
516 if nx_chk_side_of(mp) == opp { return 1 }
517 }
518 }
519 }
520 }
521 return 0
522}
523
524func nx_chk_jumps_exist(s: *i64, side: i64) -> i64 {
525 let buf: *i64 = (sys_mmap(192 * 8)) as *i64
526 let n: i64 = nx_chk_legal_jumps(s, side, buf)
527 if n > 0 { return 1 }
528 return 0
529}
530// ===== Combined legal-action availability check ======================
531//
532// Returns 1 if `side` has ANY legal action available (jump OR simple move
533// when no jumps are available). Returns 0 if side is in zugzwang (no
534// legal action possible) OR if outcome != ONGOING.
535//
536// Used by win-condition evaluation (C1c-5) and mandatory-capture rule
537// (C1c-3 — apply_simple_move uses jumps-exist? predicate directly).
538func nx_chk_has_any_legal_move(s: *i64, side: i64) -> i64 {
539 if s[NX_CHK_OFF_OUTCOME] != NX_CHK_ONGOING { return 0 }
540 // C1c-4: during a forced chain, only the chain piece may act -- and only
541 // via jump. has_any_legal_move reflects that constraint.
542 let fc: i64 = s[NX_CHK_OFF_FORCE_CONTINUE]
543 if fc >= 0 {
544 if nx_chk_piece_can_jump_from(s, fc) == 1 { return 1 }
545 return 0
546 }
547 let buf: *i64 = (sys_mmap(192 * 8)) as *i64
548 let jumps: i64 = nx_chk_legal_jumps(s, side, buf)
549 if jumps > 0 { return 1 }
550 let moves: i64 = nx_chk_legal_moves(s, side, buf)
551 if moves > 0 { return 1 }
552 return 0
553}
554// ===== Win-condition evaluation =======================================
555//
556// Updates s[OUTCOME] based on current state. Idempotent: if outcome is
557// already decided, does nothing. Otherwise: if the side WHOSE TURN IT IS
558// has no legal moves, the OTHER side wins.
559//
560// Per standard checkers rules: you lose if you cannot make a legal move
561// on your turn (whether because you have no pieces or because all your
562// pieces are blocked). This is the canonical "no moves = loss" rule.
563func nx_chk_check_outcome(s: *i64) {
564 if s[NX_CHK_OFF_OUTCOME] != NX_CHK_ONGOING { return }
565 let side: i64 = s[NX_CHK_OFF_TURN]
566 if nx_chk_has_any_legal_move(s, side) == 1 { return }
567 // Current side has no moves -- the other side wins.
568 let winner: i64 = nx_chk_other(side)
569 if winner == NX_CHK_RED { s[NX_CHK_OFF_OUTCOME] = NX_CHK_WIN_RED }
570 if winner == NX_CHK_BLACK { s[NX_CHK_OFF_OUTCOME] = NX_CHK_WIN_BLACK }
571}
572// ===== Simple-move application (no captures, no chaining) =============
573//
574// Applies a single-step diagonal move from (from_row, from_col) to
575// (to_row, to_col). Returns 1 if applied, 0 if illegal. Illegal moves
576// leave the state untouched.
577//
578// Legality:
579// - Both squares in bounds
580// - Both squares dark
581// - Source contains a piece of the current turn's side
582// - Destination is empty
583// - |dr| == 1 AND |dc| == 1 (single diagonal step)
584// - Men move forward only (red dr<0, black dr>0)
585// - Kings move either direction
586//
587// On success:
588// - Source square cleared
589// - Destination receives the piece (promoted to king if it lands on
590// the far row for its side: red man on row 0 -> red king; black man
591// on row 7 -> black king)
592// - Turn flipped
593// - plies++
594// - outcome stays ONGOING (win detection deferred to later sub-stage)
595//
596// Captures NOT yet supported -- C1b will add nx_chk_apply_jump.
597func nx_chk_apply_simple_move(s: *i64, from_row: i64, from_col: i64,
598 to_row: i64, to_col: i64) -> i64 {
599 if nx_chk_in_bounds(from_row, from_col) == 0 { return 0 }
600 if nx_chk_in_bounds(to_row, to_col) == 0 { return 0 }
601 if nx_chk_is_dark(from_row, from_col) == 0 { return 0 }
602 if nx_chk_is_dark(to_row, to_col) == 0 { return 0 }
603 if s[NX_CHK_OFF_OUTCOME] != NX_CHK_ONGOING { return 0 }
604
605 let from_sq: i64 = nx_chk_sq(from_row, from_col)
606 let to_sq: i64 = nx_chk_sq(to_row, to_col)
607 let piece: i64 = s[from_sq]
608 let dest: i64 = s[to_sq]
609 let turn: i64 = s[NX_CHK_OFF_TURN]
610
611 // Source must hold a piece of the current side.
612 if nx_chk_side_of(piece) != turn { return 0 }
613 // Destination must be empty.
614 if dest != NX_CHK_EMPTY { return 0 }
615
616 // C1c-4: during a forced multi-jump chain, no simple move is legal.
617 if s[NX_CHK_OFF_FORCE_CONTINUE] >= 0 { return 0 }
618
619 // Mandatory-capture rule (C1c-3): if any jump is available for the
620 // current side, simple moves are illegal. The substrate enforces;
621 // callers cannot bypass. Per standard American checkers rules.
622 if nx_chk_jumps_exist(s, turn) == 1 { return 0 }
623
624 // Diagonal step of exactly 1.
625 let dr: i64 = to_row - from_row
626 let dc: i64 = to_col - from_col
627 var dr_abs: i64 = dr
628 if dr < 0 { dr_abs = 0 - dr }
629 var dc_abs: i64 = dc
630 if dc < 0 { dc_abs = 0 - dc }
631 if dr_abs != 1 { return 0 }
632 if dc_abs != 1 { return 0 }
633
634 // Men can only move forward. Red forward is row-decreasing (dr=-1);
635 // black forward is row-increasing (dr=+1). Kings move either way.
636 if piece == NX_CHK_RED_MAN {
637 if dr != -1 { return 0 }
638 }
639 if piece == NX_CHK_BLACK_MAN {
640 if dr != 1 { return 0 }
641 }
642
643 // All checks pass. Apply.
644 var moved: i64 = piece
645 // Promotion: red man reaching row 0 -> red king; black man reaching row 7 -> black king.
646 if piece == NX_CHK_RED_MAN {
647 if to_row == 0 { moved = NX_CHK_RED_KING }
648 }
649 if piece == NX_CHK_BLACK_MAN {
650 if to_row == 7 { moved = NX_CHK_BLACK_KING }
651 }
652 s[from_sq] = NX_CHK_EMPTY
653 s[to_sq] = moved
654 s[NX_CHK_OFF_TURN] = nx_chk_other(turn)
655 s[NX_CHK_OFF_PLIES] = s[NX_CHK_OFF_PLIES] + 1
656 // Win-condition evaluation (C1c-5): if the new turn's side has no
657 // legal moves, the side that just moved wins.
658 nx_chk_check_outcome(s)
659 return 1
660}
661// ===== Jump application (single capture; no chaining yet) ============
662//
663// Applies a single diagonal-2 jump from (from_row, from_col) to
664// (to_row, to_col), capturing the piece at the midpoint. Returns 1 if
665// applied, 0 if illegal. Illegal jumps leave state untouched.
666//
667// Legality:
668// - Both squares in bounds + dark
669// - Source contains a piece of the current turn's side
670// - Destination is empty
671// - |dr| == 2 AND |dc| == 2 (exactly a diagonal-2 step)
672// - Midpoint (from + step) contains an OPPONENT piece (not own, not empty)
673// - Man can only jump forward; king can jump either direction
674//
675// On success:
676// - Source square cleared
677// - Midpoint cleared (captured piece removed)
678// - Destination receives the moving piece, promoted to king if it lands
679// on the far row
680// - Turn flipped (NOTE: this sub-stage does NOT yet handle multi-jump
681// chaining; force_continue stays -1. C1c-4 will add chaining.)
682// - plies++
683//
684// MANDATORY-CAPTURE rule NOT yet enforced in this sub-stage (C1c-3).
685// Callers can use simple moves even when jumps are available; that will
686// become illegal in C1c-3.
687func nx_chk_apply_jump(s: *i64, from_row: i64, from_col: i64,
688 to_row: i64, to_col: i64) -> i64 {
689 if nx_chk_in_bounds(from_row, from_col) == 0 { return 0 }
690 if nx_chk_in_bounds(to_row, to_col) == 0 { return 0 }
691 if nx_chk_is_dark(from_row, from_col) == 0 { return 0 }
692 if nx_chk_is_dark(to_row, to_col) == 0 { return 0 }
693 if s[NX_CHK_OFF_OUTCOME] != NX_CHK_ONGOING { return 0 }
694
695 let from_sq: i64 = nx_chk_sq(from_row, from_col)
696 let to_sq: i64 = nx_chk_sq(to_row, to_col)
697 let piece: i64 = s[from_sq]
698 let dest: i64 = s[to_sq]
699 let turn: i64 = s[NX_CHK_OFF_TURN]
700
701 if nx_chk_side_of(piece) != turn { return 0 }
702 if dest != NX_CHK_EMPTY { return 0 }
703
704 // C1c-4: during a forced multi-jump chain, only the chain piece can jump.
705 let fc_check: i64 = s[NX_CHK_OFF_FORCE_CONTINUE]
706 if fc_check >= 0 {
707 if from_sq != fc_check { return 0 }
708 }
709
710 // Diagonal step of exactly 2.
711 let dr: i64 = to_row - from_row
712 let dc: i64 = to_col - from_col
713 var dr_abs: i64 = dr
714 if dr < 0 { dr_abs = 0 - dr }
715 var dc_abs: i64 = dc
716 if dc < 0 { dc_abs = 0 - dc }
717 if dr_abs != 2 { return 0 }
718 if dc_abs != 2 { return 0 }
719
720 // Men jump forward only.
721 if piece == NX_CHK_RED_MAN {
722 if dr != -2 { return 0 }
723 }
724 if piece == NX_CHK_BLACK_MAN {
725 if dr != 2 { return 0 }
726 }
727
728 // Midpoint must contain an opponent piece.
729 var mid_dr: i64 = 1
730 if dr < 0 { mid_dr = -1 }
731 var mid_dc: i64 = 1
732 if dc < 0 { mid_dc = -1 }
733 let mid_row: i64 = from_row + mid_dr
734 let mid_col: i64 = from_col + mid_dc
735 let mid_sq: i64 = nx_chk_sq(mid_row, mid_col)
736 let mid_piece: i64 = s[mid_sq]
737 if mid_piece == NX_CHK_EMPTY { return 0 }
738 let opponent: i64 = nx_chk_other(turn)
739 if nx_chk_side_of(mid_piece) != opponent { return 0 }
740
741 // All legality checks passed. Apply.
742 var moved: i64 = piece
743 if piece == NX_CHK_RED_MAN {
744 if to_row == 0 { moved = NX_CHK_RED_KING }
745 }
746 if piece == NX_CHK_BLACK_MAN {
747 if to_row == 7 { moved = NX_CHK_BLACK_KING }
748 }
749 s[from_sq] = NX_CHK_EMPTY
750 s[mid_sq] = NX_CHK_EMPTY // captured piece removed
751 s[to_sq] = moved
752 s[NX_CHK_OFF_PLIES] = s[NX_CHK_OFF_PLIES] + 1
753
754 // C1c-4: chain detection. American checkers rule: promotion mid-chain
755 // STOPS the chain (piece becomes king at the end of the move). Otherwise,
756 // if the same piece can jump again from its new square, the chain
757 // continues (turn does NOT flip; force_continue marks the chain piece).
758 var promoted: i64 = 0
759 if piece != moved { promoted = 1 } // piece type changed -> promotion this move
760
761 var chain_continues: i64 = 0
762 if promoted == 0 {
763 if nx_chk_piece_can_jump_from(s, to_sq) == 1 { chain_continues = 1 }
764 }
765
766 if chain_continues == 1 {
767 // Same side keeps the turn; the chain piece must keep jumping.
768 s[NX_CHK_OFF_FORCE_CONTINUE] = to_sq
769 // No turn flip, no check_outcome -- the side's move isn't over yet.
770 } else {
771 s[NX_CHK_OFF_TURN] = opponent
772 s[NX_CHK_OFF_FORCE_CONTINUE] = -1
773 nx_chk_check_outcome(s)
774 }
775 return 1
776}
777
778// ===== Unified move application (auto-dispatches jump vs simple) ======
779//
780// Convenience wrapper: given (from, to), decides whether it's a jump
781// (|dr|=2) or simple move (|dr|=1) and dispatches to the right apply
782// function. Returns 1 if applied, 0 if illegal. UI + AI use this so
783// they don't need to encode the distinction.
784func nx_chk_apply_move(s: *i64, from_row: i64, from_col: i64,
785 to_row: i64, to_col: i64) -> i64 {
786 let dr: i64 = to_row - from_row
787 var dr_abs: i64 = dr
788 if dr < 0 { dr_abs = 0 - dr }
789 if dr_abs == 2 { return nx_chk_apply_jump(s, from_row, from_col, to_row, to_col) }
790 if dr_abs == 1 { return nx_chk_apply_simple_move(s, from_row, from_col, to_row, to_col) }
791 return 0
792}
793
794// ===== AI: easy tier (uniform random over legal actions) ==============
795//
796// Selects a legal action and writes it to out_move[0..3] = (from_row,
797// from_col, to_row, to_col). Prefers jumps (mandatory-capture rule) and
798// respects force_continue (chain piece only) automatically via legal_jumps
799// + legal_moves. Returns 1 if a move was selected, 0 if no legal action
800// (game-over state -- check_outcome would have set the winner).
801func nx_chk_pick_easy(s: *i64, side: i64, prng_state: *i64, out_move: *i64) -> i64 {
802 let buf: *i64 = (sys_mmap(192 * 8)) as *i64
803 let jumps: i64 = nx_chk_legal_jumps(s, side, buf)
804 if jumps > 0 {
805 let idx: i64 = nx_prng_range(prng_state, jumps as i64)
806 out_move[0] = buf[idx * 4 + 0]
807 out_move[1] = buf[idx * 4 + 1]
808 out_move[2] = buf[idx * 4 + 2]
809 out_move[3] = buf[idx * 4 + 3]
810 return 1
811 }
812 let moves: i64 = nx_chk_legal_moves(s, side, buf)
813 if moves > 0 {
814 let idx2: i64 = nx_prng_range(prng_state, moves as i64)
815 out_move[0] = buf[idx2 * 4 + 0]
816 out_move[1] = buf[idx2 * 4 + 1]
817 out_move[2] = buf[idx2 * 4 + 2]
818 out_move[3] = buf[idx2 * 4 + 3]
819 return 1
820 }
821 return 0
822}
823
824// ===== AI dispatcher ===================================================
825// Selects + writes to out_move; UI/caller then calls nx_chk_apply_move.
826// difficulty NX_CHK_AI_MEDIUM / HARD fall through to easy for C1d
827// (deeper AI deferred -- see roadmap §22.5 nx_browser_test_runner +
828// future minimax-depth-N work).
829func nx_chk_pick(s: *i64, side: i64, difficulty: i64,
830 prng_state: *i64, out_move: *i64) -> i64 {
831 return nx_chk_pick_easy(s, side, prng_state, out_move)
832}