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) · <a href=\"?v=1\">try v1</a> · <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}