code wiki / (root) / nx_checkers.nx

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}