code wiki / (root) / nx_tictactoe_render.nx

nx_tictactoe_render.nx source

↩ module page · 310 lines · 12025 B

1// nx_tictactoe_render.nx -- bits-up HTML rendering + event dispatch for /tictactoe. 2// 3// STATUS 2026-05-20: ARCHITECTURE-COMPLETE BUT BLOCKED ON SUBSTRATE GAPS. 4// 5// This file describes the intended bits-up rendering architecture; it 6// compiles to RISC-V asm (qemu green path) but **does NOT yet compile to 7// WAT** due to two substrate-side gaps: 8// 9// 1. nxc2 --target wat: "unhandled opcode 30 in emit_instr" -- the 10// WAT backend does not yet lower string-literal data into WASM 11// data segments. Same gap blocks nx_html_emit compilation to WAT. 12// Fix: extend nxc2/wasm.c emit_instr switch + add data-segment 13// synthesis from string-literal IR nodes. 14// 15// 2. @ifdef TARGET_X86_64 / @endif blocks in nx_syscalls.nx report 16// "2 unclosed @ifdef/@ifndef block(s)" under --target wat as of 17// 2026-05-20 background changes. Macro expansion regression. 18// 19// Until both close, the /tictactoe page uses minimal bespoke JS for DOM 20// rendering with NishiLang-compiled WASM for game logic + AI. When the 21// gaps close, this file gets compiled into the WASM module and the bespoke 22// JS gets replaced by dist/_nishi/nishi-host.js calling nx_ttt_render + 23// nx_ttt_action via the sentinel-fd-99 convention. 24// 25// Per [[feedback-ui-rendering-bits-up-not-just-game-logic]]: UI emission + 26// click-to-state mapping live in NishiLang. Only the IRREDUCIBLE 27// dist/_nishi/nishi-host.js (~120 LOC: WASM-instantiate + syscall imports + 28// innerHTML setter + addEventListener) is JS. 29// 30// HTML is emitted by writing bytes to fd 99 (sentinel); the JS host captures 31// those bytes into a buffer and sets innerHTML. Same fd-99 convention is 32// reusable across every Nishi game. 33// 34// Exports (called by nishi-host.js): 35// nx_ttt_init() -> *i64 -- allocates + returns initial state 36// nx_ttt_render(s) -> i64 -- emits full HTML to fd 99 37// nx_ttt_action(s, act, a0, a1, a2, a3) -> i64 38// -- handles a click/change event; 39// returns 1 if state changed (host 40// re-renders), 0 otherwise. 41// 42// Action IDs (data-action="<id>"): 43// 1 = click cell, arg0 = cell index 0..8 44// 2 = reset game 45 46import "nx_syscalls.nx" 47import "nx_tier.nx" 48import "nx_tictactoe.nx" 49const NX_MAGIC_1000003: i64 = 1000003 50 51const NX_TTT_RENDER_FD: i64 = 99 52 53const NX_TTT_ACTION_CLICK_CELL: i64 = 1 54const NX_TTT_ACTION_RESET: i64 = 2 55const NX_TTT_ACTION_SET_OPPONENT: i64 = 3 56const NX_TTT_ACTION_SET_PLAYER: i64 = 4 57 58// ===== Byte writers ================================================= 59// 60// Inline strlen-equivalent + single-digit integer write. Avoids importing 61// nx_runtime (which currently trips the WAT backend's opcode-0x30 handler 62// when called from this nesting depth). Bug logged as substrate-gap for 63// the nx_wat_compiler.nx milestone in ยง22.5. 64 65func _ttt_strlen(s: *u8) -> i64 { 66 var n: i64 = 0 67 while s[n] != 0 { n = n + 1 } 68 return n 69} 70 71func _ttt_w(s: *u8) -> i64 { 72 return sys_write(NX_TTT_RENDER_FD, s, _ttt_strlen(s)) 73} 74 75// Write a single decimal digit (0..9). For tic-tac-toe cell indices 0..8 76// this is the only integer-to-string we need. 77func _ttt_wd(d: i64) -> i64 { 78 let buf: *u8 = sys_mmap(8) 79 buf[0] = (48 + d) as u8 80 return sys_write(NX_TTT_RENDER_FD, buf, 1) 81} 82 83// ===== Init + reset ================================================= 84 85func nx_ttt_init() -> *i64 { 86 return nx_ttt_new(NX_TTT_X) 87} 88 89// Reset clears the game cells (board + turn + outcome + winner) but PRESERVES 90// the UI-config cells (player_side + opponent) so changing difficulty / side 91// doesn't snap the operator back to defaults. 92func nx_ttt_reset_in_place(s: *i64) { 93 var i: i64 = 0 94 while i < 9 { s[i] = NX_TTT_EMPTY; i = i + 1 } 95 s[NX_TTT_OFF_TURN] = NX_TTT_X 96 s[NX_TTT_OFF_OUTCOME] = NX_TTT_ONGOING 97 s[NX_TTT_OFF_WINNER] = 0 98} 99 100// ===== Render to fd 99 =============================================== 101 102func _ttt_emit_status(s: *i64) -> i64 { 103 _ttt_w("<div class=\"ttt-status\" aria-live=\"polite\">" as *u8) 104 let outcome: i64 = nx_ttt_outcome(s) 105 let player_side: i64 = s[NX_TTT_OFF_PLAYER_SIDE] 106 let opponent: i64 = s[NX_TTT_OFF_OPPONENT] 107 let is_human_vs_human: i64 = 0 108 var is_h: i64 = 0 109 if opponent < 0 { is_h = 1 } 110 if outcome == NX_TTT_WIN { 111 let w: i64 = nx_ttt_winner(s) 112 if is_h == 1 { 113 if w == NX_TTT_X { _ttt_w("X wins." as *u8) } 114 if w == NX_TTT_O { _ttt_w("O wins." as *u8) } 115 } 116 if is_h == 0 { 117 if w == player_side { _ttt_w("You win." as *u8) } 118 if w != player_side { _ttt_w("AI wins." as *u8) } 119 } 120 } 121 if outcome == NX_TTT_DRAW { _ttt_w("Draw." as *u8) } 122 if outcome == NX_TTT_ONGOING { 123 let turn: i64 = nx_ttt_turn(s) 124 if is_h == 1 { 125 if turn == NX_TTT_X { _ttt_w("X to move." as *u8) } 126 if turn == NX_TTT_O { _ttt_w("O to move." as *u8) } 127 } 128 if is_h == 0 { 129 if turn == player_side { 130 if player_side == NX_TTT_X { _ttt_w("Your move (X)." as *u8) } 131 if player_side == NX_TTT_O { _ttt_w("Your move (O)." as *u8) } 132 } 133 if turn != player_side { _ttt_w("AI thinking..." as *u8) } 134 } 135 } 136 _ttt_w("</div>" as *u8) 137 return 0 138} 139 140// Helper: emit one <option> tag with selected attribute when the option's 141// stored numeric value matches the current setting. 142func _ttt_emit_opt(value: i64, is_selected: i64, label: *u8) -> i64 { 143 _ttt_w("<option value=\"" as *u8) 144 // value can be negative (-1 for human); emit as text. Range is -1..2, 145 // so a tiny inline emitter is enough. 146 if value == -1 { _ttt_w("-1" as *u8) } 147 if value == 0 { _ttt_w("0" as *u8) } 148 if value == 1 { _ttt_w("1" as *u8) } 149 if value == 2 { _ttt_w("2" as *u8) } 150 _ttt_w("\"" as *u8) 151 if is_selected == 1 { _ttt_w(" selected" as *u8) } 152 _ttt_w(">" as *u8) 153 _ttt_w(label) 154 _ttt_w("</option>" as *u8) 155 return 0 156} 157 158func _ttt_emit_controls(s: *i64) -> i64 { 159 let player_side: i64 = s[NX_TTT_OFF_PLAYER_SIDE] 160 let opponent: i64 = s[NX_TTT_OFF_OPPONENT] 161 _ttt_w("<div class=\"controls\">" as *u8) 162 _ttt_w("<button type=\"button\" data-action=\"2\">New game</button>" as *u8) 163 _ttt_w("<label>Opponent: <select data-action=\"3\">" as *u8) 164 var sel: i64 = 0 165 if opponent == -1 { sel = 1 } 166 _ttt_emit_opt(-1 as i64, sel, "Another human" as *u8) 167 sel = 0 168 if opponent == NX_TTT_AI_EASY { sel = 1 } 169 _ttt_emit_opt(NX_TTT_AI_EASY, sel, "AI - easy (random)" as *u8) 170 sel = 0 171 if opponent == NX_TTT_AI_MEDIUM { sel = 1 } 172 _ttt_emit_opt(NX_TTT_AI_MEDIUM, sel, "AI - medium (1-ply)" as *u8) 173 sel = 0 174 if opponent == NX_TTT_AI_PERFECT { sel = 1 } 175 _ttt_emit_opt(NX_TTT_AI_PERFECT, sel, "AI - perfect (minimax)" as *u8) 176 _ttt_w("</select></label>" as *u8) 177 178 _ttt_w("<label>I play: <select data-action=\"4\">" as *u8) 179 sel = 0 180 if player_side == NX_TTT_X { sel = 1 } 181 _ttt_emit_opt(NX_TTT_X, sel, "X (first)" as *u8) 182 sel = 0 183 if player_side == NX_TTT_O { sel = 1 } 184 _ttt_emit_opt(NX_TTT_O, sel, "O (second)" as *u8) 185 _ttt_w("</select></label>" as *u8) 186 _ttt_w("</div>" as *u8) 187 return 0 188} 189 190func _ttt_emit_cell(s: *i64, idx: i64) -> i64 { 191 let v: i64 = nx_ttt_cell(s, idx) 192 let outcome: i64 = nx_ttt_outcome(s) 193 _ttt_w("<button type=\"button\" class=\"cell" as *u8) 194 if v == NX_TTT_X { _ttt_w(" cell-x" as *u8) } 195 if v == NX_TTT_O { _ttt_w(" cell-o" as *u8) } 196 _ttt_w("\" role=\"gridcell\" data-cell-idx=\"" as *u8) 197 _ttt_wd(idx) 198 _ttt_w("\" data-action=\"1\" data-arg0=\"" as *u8) 199 _ttt_wd(idx) 200 _ttt_w("\"" as *u8) 201 if v != NX_TTT_EMPTY { _ttt_w(" disabled" as *u8) } 202 if outcome != NX_TTT_ONGOING { _ttt_w(" disabled" as *u8) } 203 _ttt_w(">" as *u8) 204 if v == NX_TTT_X { _ttt_w("X" as *u8) } 205 if v == NX_TTT_O { _ttt_w("O" as *u8) } 206 _ttt_w("</button>" as *u8) 207 return 0 208} 209 210// Sovereign version banner. Emits a label + an `<a href="?v=1">` link to 211// the previous snapshot. Pure browser navigation -- no JS dispatch. The 212// current source-of-truth is v2 (this build). See 213// [[project-sovereign-version-pinning]]. 214func _ttt_emit_version_banner() -> i64 { 215 _ttt_w("<div class=\"version-banner\" style=\"font-size:.8rem;color:#888;margin-bottom:.4rem;\">" as *u8) 216 _ttt_w("Version 2 (current) &middot; <a href=\"?v=1\">try v1</a> &middot; <a href=\"./\">latest</a>" as *u8) 217 _ttt_w("</div>" as *u8) 218 return 0 219} 220 221func nx_ttt_render(s: *i64) -> i64 { 222 _ttt_w("<div class=\"ttt-shell\">" as *u8) 223 _ttt_emit_version_banner() 224 _ttt_emit_status(s) 225 _ttt_w("<div class=\"board\" role=\"grid\" aria-label=\"Tic-tac-toe board\">" as *u8) 226 var i: i64 = 0 227 while i < 9 { 228 _ttt_emit_cell(s, i) 229 i = i + 1 230 } 231 _ttt_w("</div>" as *u8) 232 _ttt_emit_controls(s) 233 _ttt_w("</div>" as *u8) 234 return 0 235} 236 237// ===== Action dispatcher ============================================ 238 239// Dispatcher: select the AI move per the stored opponent tier. side = the 240// side the AI is playing (which is the side that's NOT the player's). 241// Returns the chosen cell (-1 if no legal move). 242func _ttt_ai_pick(s: *i64, side: i64) -> i64 { 243 let opp: i64 = s[NX_TTT_OFF_OPPONENT] 244 if opp == NX_TTT_AI_PERFECT { return nx_ttt_pick_perfect(s, side) } 245 if opp == NX_TTT_AI_MEDIUM { return nx_ttt_pick_medium(s, side) } 246 // Easy (random) tier needs a PRNG state. For host-driven environments 247 // the host owns the PRNG; the renderer needs a small stable one. Use 248 // the state's plies/winner cells as a seed-mixer so behavior is 249 // deterministic per state without external entropy. 250 if opp == NX_TTT_AI_EASY { 251 let mix: i64 = (s[NX_TTT_OFF_TURN] * NX_MAGIC_1000003) ^ (nx_ttt_state_hash(s) as i64) 252 var prng_state: i64 = mix 253 if prng_state == 0 { prng_state = 1 } 254 let p: *i64 = (&prng_state) as *i64 255 return nx_ttt_pick_easy(s, p) 256 } 257 // Default fallback: perfect. 258 return nx_ttt_pick_perfect(s, side) 259} 260 261func nx_ttt_action(s: *i64, action: i64, arg0: i64, arg1: i64, 262 arg2: i64, arg3: i64) -> i64 { 263 if action == NX_TTT_ACTION_CLICK_CELL { 264 // Player plays at arg0; if game still ongoing AND opponent is AI, 265 // AI responds with the configured tier. 266 let applied: i64 = nx_ttt_try_apply(s, arg0) 267 if applied != 1 { return 0 } 268 let opp: i64 = s[NX_TTT_OFF_OPPONENT] 269 if opp >= 0 { 270 if nx_ttt_outcome(s) == NX_TTT_ONGOING { 271 let ai_side: i64 = nx_ttt_other(s[NX_TTT_OFF_PLAYER_SIDE]) 272 let mv: i64 = _ttt_ai_pick(s, ai_side) 273 if mv >= 0 { nx_ttt_try_apply(s, mv) } 274 } 275 } 276 return 1 277 } 278 if action == NX_TTT_ACTION_RESET { 279 nx_ttt_reset_in_place(s) 280 return 1 281 } 282 if action == NX_TTT_ACTION_SET_OPPONENT { 283 s[NX_TTT_OFF_OPPONENT] = arg0 284 nx_ttt_reset_in_place(s) 285 // If AI plays X (opponent != human AND player_side != X), AI moves first. 286 let opp_after: i64 = s[NX_TTT_OFF_OPPONENT] 287 let ps: i64 = s[NX_TTT_OFF_PLAYER_SIDE] 288 if opp_after >= 0 { 289 if ps != NX_TTT_X { 290 let mv: i64 = _ttt_ai_pick(s, NX_TTT_X) 291 if mv >= 0 { nx_ttt_try_apply(s, mv) } 292 } 293 } 294 return 1 295 } 296 if action == NX_TTT_ACTION_SET_PLAYER { 297 s[NX_TTT_OFF_PLAYER_SIDE] = arg0 298 nx_ttt_reset_in_place(s) 299 // If player is O, AI moves first as X. 300 let opp_after2: i64 = s[NX_TTT_OFF_OPPONENT] 301 if opp_after2 >= 0 { 302 if arg0 != NX_TTT_X { 303 let mv: i64 = _ttt_ai_pick(s, NX_TTT_X) 304 if mv >= 0 { nx_ttt_try_apply(s, mv) } 305 } 306 } 307 return 1 308 } 309 return 0 310}