code wiki / (root) / nx_checkers_render.nx

nx_checkers_render.nx source

↩ module page · 379 lines · 14248 B

1// nx_checkers_render.nx -- bits-up HTML rendering + event dispatch for /checkers. 2// 3// Mirrors nx_tictactoe_render.nx pattern. HTML emitted via sys_write to 4// sentinel fd 99; dist/_nishi/nishi-host.js captures + sets innerHTML. 5// Click events route via data-action="<id>" to nx_chk_action. 6// 7// State block carries one extra "selected square" UI cell after the game 8// cells so the renderer can highlight the selected source-piece + show 9// legal destinations. Hash of game state stays in cells 0..67 (unchanged). 10// 11// Exports: 12// nx_chk_render_init() -> *i64 13// nx_chk_render(s) -> i64 -- emits HTML to fd 99 14// nx_chk_action(s, act, a0, a1, a2, a3) -> i64 15// 16// Action IDs (data-action): 17// 1 = click square N (arg0 = 0..63) 18// 2 = reset game 19// 3 = set opponent (arg0: -1=human, 0=easy) 20// 4 = set player side (arg0: 1=red, 2=black) 21 22import "nx_syscalls.nx" 23import "nx_tier.nx" 24import "nx_prng.nx" 25import "nx_checkers.nx" 26const NX_MAGIC_12345: i64 = 12345 27 28const NX_CHK_RENDER_FD: i64 = 99 29 30const NX_CHK_ACTION_CLICK_SQ: i64 = 1 31const NX_CHK_ACTION_RESET: i64 = 2 32const NX_CHK_ACTION_SET_OPPONENT: i64 = 3 33const NX_CHK_ACTION_SET_PLAYER: i64 = 4 34 35// State layout adds 4 config cells beyond the 68-cell game state. 36const NX_CHK_OFF_SELECTED: i64 = 68 // -1 if no selection 37const NX_CHK_OFF_PLAYER_SIDE: i64 = 69 // NX_CHK_RED or NX_CHK_BLACK 38const NX_CHK_OFF_OPPONENT: i64 = 70 // -1=human, 0=easy 39const NX_CHK_OFF_PRNG: i64 = 71 // pointer (as i64) to separately-allocated PRNG state 40const NX_CHK_RENDER_CELLS: i64 = 72 41 42// ===== Byte writers ================================================= 43 44func _chk_strlen(s: *u8) -> i64 { 45 var n: i64 = 0 46 while s[n] != 0 { n = n + 1 } 47 return n 48} 49 50func _chk_w(s: *u8) -> i64 { 51 return sys_write(NX_CHK_RENDER_FD, s, _chk_strlen(s)) 52} 53 54// Write a 1- or 2-digit non-negative integer (covers 0..63). 55func _chk_wn(n: i64) -> i64 { 56 let buf: *u8 = sys_mmap(8) 57 if n < 10 { 58 buf[0] = (48 + n) as u8 59 return sys_write(NX_CHK_RENDER_FD, buf, 1) 60 } 61 let tens: i64 = n / 10 62 let ones: i64 = n - tens * 10 63 buf[0] = (48 + tens) as u8 64 buf[1] = (48 + ones) as u8 65 return sys_write(NX_CHK_RENDER_FD, buf, 2) 66} 67 68// ===== Init + reset ================================================= 69 70func nx_chk_render_init() -> *i64 { 71 // Allocate the larger render-aware state block. 72 let s: *i64 = (sys_mmap(NX_CHK_RENDER_CELLS * 8)) as *i64 73 // Initialize the game cells via the standard nx_chk_new -> copy approach. 74 // (nx_chk_new allocates only 68 cells; we copy them into our block.) 75 let g: *i64 = nx_chk_new() 76 var i: i64 = 0 77 while i < NX_CHK_STATE_CELLS { 78 s[i] = g[i] 79 i = i + 1 80 } 81 s[NX_CHK_OFF_SELECTED] = -1 82 s[NX_CHK_OFF_PLAYER_SIDE] = NX_CHK_RED 83 s[NX_CHK_OFF_OPPONENT] = NX_CHK_AI_EASY // default to playable-not-too-hard 84 let prng_ptr: *i64 = (sys_mmap(8)) as *i64 85 nx_prng_init(prng_ptr, NX_MAGIC_12345 as i64) 86 s[NX_CHK_OFF_PRNG] = prng_ptr as i64 87 return s 88} 89 90func _chk_reset_in_place(s: *i64) { 91 // Re-fill the game cells via a fresh nx_chk_new. 92 let g: *i64 = nx_chk_new() 93 var i: i64 = 0 94 while i < NX_CHK_STATE_CELLS { 95 s[i] = g[i] 96 i = i + 1 97 } 98 s[NX_CHK_OFF_SELECTED] = -1 99} 100 101// ===== Helpers ====================================================== 102 103func _chk_is_dest_legal(s: *i64, from_sq: i64, to_sq: i64) -> i64 { 104 // Probe: try the move on a SCRATCH copy of the state; if it applies, 105 // the move is legal. Pure side-effect-free legality check. 106 let probe: *i64 = (sys_mmap(NX_CHK_STATE_CELLS * 8)) as *i64 107 var i: i64 = 0 108 while i < NX_CHK_STATE_CELLS { probe[i] = s[i]; i = i + 1 } 109 let fr: i64 = from_sq / 8 110 let fc: i64 = from_sq - fr * 8 111 let tr: i64 = to_sq / 8 112 let tc: i64 = to_sq - tr * 8 113 return nx_chk_apply_move(probe, fr, fc, tr, tc) 114} 115 116// ===== Status ======================================================= 117 118func _chk_emit_status(s: *i64) -> i64 { 119 _chk_w("<div class=\"chk-status\" aria-live=\"polite\">" as *u8) 120 let outcome: i64 = nx_chk_outcome(s) 121 let player_side: i64 = s[NX_CHK_OFF_PLAYER_SIDE] 122 let opponent: i64 = s[NX_CHK_OFF_OPPONENT] 123 var is_h: i64 = 0 124 if opponent < 0 { is_h = 1 } 125 if outcome == NX_CHK_WIN_RED { 126 if is_h == 1 { _chk_w("Red wins." as *u8) } 127 if is_h == 0 { 128 if player_side == NX_CHK_RED { _chk_w("You win." as *u8) } 129 if player_side != NX_CHK_RED { _chk_w("AI wins." as *u8) } 130 } 131 } 132 if outcome == NX_CHK_WIN_BLACK { 133 if is_h == 1 { _chk_w("Black wins." as *u8) } 134 if is_h == 0 { 135 if player_side == NX_CHK_BLACK { _chk_w("You win." as *u8) } 136 if player_side != NX_CHK_BLACK { _chk_w("AI wins." as *u8) } 137 } 138 } 139 if outcome == NX_CHK_DRAW { _chk_w("Draw." as *u8) } 140 if outcome == NX_CHK_ONGOING { 141 let turn: i64 = nx_chk_turn(s) 142 if is_h == 1 { 143 if turn == NX_CHK_RED { _chk_w("Red to move." as *u8) } 144 if turn == NX_CHK_BLACK { _chk_w("Black to move." as *u8) } 145 } 146 if is_h == 0 { 147 if turn == player_side { 148 if player_side == NX_CHK_RED { _chk_w("Your move (Red)." as *u8) } 149 if player_side == NX_CHK_BLACK { _chk_w("Your move (Black)." as *u8) } 150 } 151 if turn != player_side { _chk_w("AI thinking..." as *u8) } 152 } 153 } 154 _chk_w("</div>" as *u8) 155 return 0 156} 157 158// ===== Board ========================================================= 159 160func _chk_emit_piece(piece: i64) -> i64 { 161 if piece == NX_CHK_RED_MAN { _chk_w("<div class=\"piece red\"></div>" as *u8) } 162 if piece == NX_CHK_RED_KING { _chk_w("<div class=\"piece red king\">K</div>" as *u8) } 163 if piece == NX_CHK_BLACK_MAN { _chk_w("<div class=\"piece black\"></div>" as *u8) } 164 if piece == NX_CHK_BLACK_KING { _chk_w("<div class=\"piece black king\">K</div>" as *u8) } 165 return 0 166} 167 168func _chk_emit_board(s: *i64) -> i64 { 169 let selected: i64 = s[NX_CHK_OFF_SELECTED] 170 _chk_w("<div class=\"chk-board\" role=\"grid\" aria-label=\"Checkers board\">" as *u8) 171 var row: i64 = 0 172 while row < 8 { 173 var col: i64 = 0 174 while col < 8 { 175 let sq_idx: i64 = row * 8 + col 176 let is_dark: i64 = nx_chk_is_dark(row, col) 177 let piece: i64 = s[sq_idx] 178 // dark squares are clickable; lights are static. 179 if is_dark == 1 { 180 var classes_extra: *u8 = "" as *u8 181 if selected == sq_idx { classes_extra = " sel" as *u8 } 182 var hint: i64 = 0 183 if selected >= 0 { 184 if _chk_is_dest_legal(s, selected, sq_idx) == 1 { hint = 1 } 185 } 186 _chk_w("<button type=\"button\" class=\"sq dark" as *u8) 187 if selected == sq_idx { _chk_w(" sel" as *u8) } 188 if hint == 1 { _chk_w(" hint" as *u8) } 189 _chk_w("\" role=\"gridcell\" data-sq=\"" as *u8) 190 _chk_wn(sq_idx) 191 _chk_w("\" data-action=\"1\" data-arg0=\"" as *u8) 192 _chk_wn(sq_idx) 193 _chk_w("\">" as *u8) 194 _chk_emit_piece(piece) 195 _chk_w("</button>" as *u8) 196 } 197 if is_dark == 0 { 198 _chk_w("<div class=\"sq light\" data-sq=\"" as *u8) 199 _chk_wn(sq_idx) 200 _chk_w("\"></div>" as *u8) 201 } 202 col = col + 1 203 } 204 row = row + 1 205 } 206 _chk_w("</div>" as *u8) 207 return 0 208} 209 210// ===== Controls ====================================================== 211 212func _chk_emit_opt(value: i64, is_selected: i64, label: *u8) -> i64 { 213 _chk_w("<option value=\"" as *u8) 214 if value == -1 { _chk_w("-1" as *u8) } 215 if value == 0 { _chk_w("0" as *u8) } 216 if value == 1 { _chk_w("1" as *u8) } 217 if value == 2 { _chk_w("2" as *u8) } 218 _chk_w("\"" as *u8) 219 if is_selected == 1 { _chk_w(" selected" as *u8) } 220 _chk_w(">" as *u8) 221 _chk_w(label) 222 _chk_w("</option>" as *u8) 223 return 0 224} 225 226func _chk_emit_controls(s: *i64) -> i64 { 227 let player_side: i64 = s[NX_CHK_OFF_PLAYER_SIDE] 228 let opponent: i64 = s[NX_CHK_OFF_OPPONENT] 229 _chk_w("<div class=\"controls\">" as *u8) 230 _chk_w("<button type=\"button\" data-action=\"2\">New game</button>" as *u8) 231 _chk_w("<label>Opponent: <select data-action=\"3\">" as *u8) 232 var sel: i64 = 0 233 if opponent == -1 { sel = 1 } 234 _chk_emit_opt(-1 as i64, sel, "Another human" as *u8) 235 sel = 0 236 if opponent == NX_CHK_AI_EASY { sel = 1 } 237 _chk_emit_opt(NX_CHK_AI_EASY, sel, "AI - easy (random)" as *u8) 238 _chk_w("</select></label>" as *u8) 239 240 _chk_w("<label>I play: <select data-action=\"4\">" as *u8) 241 sel = 0 242 if player_side == NX_CHK_RED { sel = 1 } 243 _chk_emit_opt(NX_CHK_RED, sel, "Red (first)" as *u8) 244 sel = 0 245 if player_side == NX_CHK_BLACK { sel = 1 } 246 _chk_emit_opt(NX_CHK_BLACK, sel, "Black (second)" as *u8) 247 _chk_w("</select></label>" as *u8) 248 _chk_w("</div>" as *u8) 249 return 0 250} 251 252// ===== Render top-level =============================================== 253 254// Sovereign version banner. Emits a label + an `<a href="?v=1">` link to 255// the previous snapshot. Pure browser navigation -- no JS dispatch. The 256// current source-of-truth is v2 (this build). 257func _chk_emit_version_banner() -> i64 { 258 _chk_w("<div class=\"version-banner\" style=\"font-size:.8rem;color:#888;margin-bottom:.4rem;\">" as *u8) 259 _chk_w("Version 2 (current) &middot; <a href=\"?v=1\">try v1</a> &middot; <a href=\"./\">latest</a>" as *u8) 260 _chk_w("</div>" as *u8) 261 return 0 262} 263 264func nx_chk_render(s: *i64) -> i64 { 265 _chk_w("<div class=\"chk-shell\">" as *u8) 266 _chk_emit_version_banner() 267 _chk_emit_status(s) 268 _chk_emit_board(s) 269 _chk_emit_controls(s) 270 _chk_w("</div>" as *u8) 271 return 0 272} 273 274// ===== Action dispatcher ============================================= 275// 276// Click on a dark square: if no current selection, select THIS square (must 277// be your piece + your turn). If a selection exists, attempt the move 278// from selection -> here; if the move applies, clear selection (or keep it 279// on the new square if force_continue is set for multi-jump chain). If 280// the move fails, switch selection to the new square if it's still your 281// piece, else clear. 282func _chk_handle_click(s: *i64, sq_idx: i64) -> i64 { 283 let outcome: i64 = nx_chk_outcome(s) 284 if outcome != NX_CHK_ONGOING { return 0 } 285 let turn: i64 = nx_chk_turn(s) 286 let player_side: i64 = s[NX_CHK_OFF_PLAYER_SIDE] 287 let opponent: i64 = s[NX_CHK_OFF_OPPONENT] 288 // Only the player's side can click (AI side is handled programmatically). 289 if opponent >= 0 { 290 if turn != player_side { return 0 } 291 } 292 let piece_here: i64 = s[sq_idx] 293 let side_here: i64 = nx_chk_side_of(piece_here) 294 let selected: i64 = s[NX_CHK_OFF_SELECTED] 295 if selected < 0 { 296 // No selection -- if this square has the current side's piece, select it. 297 if side_here == turn { 298 s[NX_CHK_OFF_SELECTED] = sq_idx 299 return 1 300 } 301 return 0 302 } 303 // Selection exists -- try the move from selected -> sq_idx. 304 let fr: i64 = selected / 8 305 let fc: i64 = selected - fr * 8 306 let tr: i64 = sq_idx / 8 307 let tc: i64 = sq_idx - tr * 8 308 let applied: i64 = nx_chk_apply_move(s, fr, fc, tr, tc) 309 if applied == 1 { 310 // If multi-jump chain still active, keep selection on the chain piece. 311 let fc_after: i64 = nx_chk_force_continue(s) 312 if fc_after >= 0 { s[NX_CHK_OFF_SELECTED] = fc_after } 313 if fc_after < 0 { s[NX_CHK_OFF_SELECTED] = -1 } 314 return 1 315 } 316 // Move didn't apply. If this square has the current side's piece, 317 // switch selection to it; else clear selection. 318 if side_here == turn { s[NX_CHK_OFF_SELECTED] = sq_idx } 319 if side_here != turn { s[NX_CHK_OFF_SELECTED] = -1 } 320 return 1 321} 322 323func _chk_ai_take_turn(s: *i64) { 324 let opp: i64 = s[NX_CHK_OFF_OPPONENT] 325 if opp < 0 { return } 326 if nx_chk_outcome(s) != NX_CHK_ONGOING { return } 327 let player_side: i64 = s[NX_CHK_OFF_PLAYER_SIDE] 328 let ai_side: i64 = nx_chk_other(player_side) 329 // Loop in case the AI's first move starts a multi-jump chain. 330 var safety: i64 = 0 331 while safety < 32 { 332 if nx_chk_outcome(s) != NX_CHK_ONGOING { return } 333 if nx_chk_turn(s) != ai_side { return } 334 let mv_buf: *i64 = (sys_mmap(4 * 8)) as *i64 335 let prng_p: *i64 = (s[NX_CHK_OFF_PRNG]) as *i64 336 let picked: i64 = nx_chk_pick(s, ai_side, opp, prng_p, mv_buf) 337 if picked != 1 { return } 338 nx_chk_apply_move(s, mv_buf[0], mv_buf[1], mv_buf[2], mv_buf[3]) 339 safety = safety + 1 340 } 341} 342 343func nx_chk_action(s: *i64, action: i64, arg0: i64, arg1: i64, 344 arg2: i64, arg3: i64) -> i64 { 345 if action == NX_CHK_ACTION_CLICK_SQ { 346 let changed: i64 = _chk_handle_click(s, arg0) 347 if changed == 0 { return 0 } 348 // After a player move, if it's the AI's turn now, let it play. 349 _chk_ai_take_turn(s) 350 return 1 351 } 352 if action == NX_CHK_ACTION_RESET { 353 _chk_reset_in_place(s) 354 // If AI plays Red (player is Black), AI moves first. 355 let ps: i64 = s[NX_CHK_OFF_PLAYER_SIDE] 356 if s[NX_CHK_OFF_OPPONENT] >= 0 { 357 if ps == NX_CHK_BLACK { _chk_ai_take_turn(s) } 358 } 359 return 1 360 } 361 if action == NX_CHK_ACTION_SET_OPPONENT { 362 s[NX_CHK_OFF_OPPONENT] = arg0 363 _chk_reset_in_place(s) 364 let ps2: i64 = s[NX_CHK_OFF_PLAYER_SIDE] 365 if arg0 >= 0 { 366 if ps2 == NX_CHK_BLACK { _chk_ai_take_turn(s) } 367 } 368 return 1 369 } 370 if action == NX_CHK_ACTION_SET_PLAYER { 371 s[NX_CHK_OFF_PLAYER_SIDE] = arg0 372 _chk_reset_in_place(s) 373 if s[NX_CHK_OFF_OPPONENT] >= 0 { 374 if arg0 == NX_CHK_BLACK { _chk_ai_take_turn(s) } 375 } 376 return 1 377 } 378 return 0 379}