nx_game_chat.nx source
↩ module page · 808 lines · 35073 B
1// nx_game_chat.nx -- C12 of the comms lane: TURN-BASED MULTIPLAYER + CHAT over the C1 store. Contract
2// symbol gc_room_wire == the /compare/comms C12 watch (milestone M5: comms visible in the games).
3// DONE-RULE (comms.plan): a game opens a room per session and two clients exchange moves and chat
4// through the C1 store; the gate proves order and delivery with the game's own rules as the referee.
5// REFEREE: nx_game_chat_gate.
6//
7// THE DESIGN: THE LOG IS THE GAME. A match is an ordinary nx_chat_store room; moves are TEXT rows
8// with the reserved body prefix mv:<n>; the CURRENT STATE is a DETERMINISTIC REPLAY of the log.
9// The move verb validates against the replayed state before appending, and the replay validates
10// AGAIN on every read -- so a row that slipped in out of turn (a race, or a client that lied) is
11// counted and IGNORED by every reader identically: replay is the referee, the wire is untrusted.
12// Store-and-forward multiplayer follows free: an offline player fetches-since-cursor and the same
13// replay produces the same board on every device (C2 sync moves whole matches between roots).
14//
15// GAME DEFINITIONS ARE DATA (knowledge/comms/games.conf): id, cols, rows, winlen, mode
16// (drop = gravity, connect-four class; place = direct cell, tic-tac-toe class). A new grid game is a
17// conf row. WRITES go through the ONE ruler -- this organ forks the nx_chat_store elf named in the
18// conf, so budgets/refusals/announcements stay with C1; READS are direct plane reads (the same split
19// C2 established). Chat rides the same room (say verb); a chat body may not start with the reserved
20// mv: prefix (refused by name). Tombstones hide CHAT from display but NEVER hide moves: a game's
21// history is its state, so replay ignores del: keys for move rows by design (stated, not accidental).
22// The emitted HTML artifact stamps itself from the LAST ROW's epoch, never the wall clock, so two
23// emits of the same log are byte-identical (deterministic artifacts).
24// license_tier: ORIGINAL No hw writes (Rule 26).
25import "nx_syscalls.nx"
26import "nx_itoa_lib.nx"
27import "nx_store_seed_lib.nx"
28import "nx_seg_store.nx"
29import "nx_tool_run.nx"
30
31const GC_EXIT_OK: i64 = 0
32const GC_EXIT_USAGE: i64 = 2
33const GC_EXIT_REFUSED: i64 = 3
34const GC_EXIT_RETRY: i64 = 4
35const GC_EXIT_CORRUPT: i64 = 5
36// structural bounds (format math + conf sanity, not tunables -- refusals announce them):
37const GC_NAME_MAX: i64 = 64
38const GC_PFX_CAP: i64 = 256
39const GC_CONF_CAP: i64 = 192
40const GC_BOARD_MAX: i64 = 4096 // conf sanity: cols*rows above this is a conf mistake, refused by name
41const GC_OUT_CAP: i64 = 65536 // fork-capture buffer
42const GC_HTML_CAP: i64 = 262144 // emit buffer
43const GC_NL: i64 = 10
44const GC_PIPE: i64 = 124
45const GC_ST_ONGOING: i64 = 0
46const GC_ST_WIN: i64 = 2
47const GC_ST_DRAW: i64 = 3
48const GC_MODE_DROP: i64 = 1
49const GC_MODE_PLACE: i64 = 2
50
51func gc_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
52func gc_n(v: i64) -> i64 { nxi_out(v); return 0 }
53func gc_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
54func gc_eq(a: *u8, b: *u8) -> i64 {
55 var i: i64 = 0
56 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
57 if b[i] != (0 as u8) { return 0 }
58 return 1
59}
60func gc_tok_ok(s: *u8) -> i64 {
61 var i: i64 = 0
62 while s[i] != (0 as u8) {
63 let c: i64 = s[i] as i64
64 var ok: i64 = 0
65 if c >= 48 { if c <= 57 { ok = 1 } }
66 if c >= 65 { if c <= 90 { ok = 1 } }
67 if c >= 97 { if c <= 122 { ok = 1 } }
68 if c == 95 { ok = 1 }
69 if ok == 0 { return 0 }
70 i = i + 1
71 }
72 if i < 1 { return 0 }
73 if i > GC_NAME_MAX { return 0 }
74 return 1
75}
76func gc_atoi(b: *u8, n: i64) -> i64 {
77 var v: i64 = 0
78 var i: i64 = 0
79 while i < n {
80 let c: i64 = b[i] as i64
81 if c < 48 { break }
82 if c > 57 { break }
83 v = v * 10 + (c - 48)
84 i = i + 1
85 }
86 return v
87}
88func gc_hexv(c: i64) -> i64 {
89 if c >= 48 { if c <= 57 { return c - 48 } }
90 if c >= 97 { if c <= 102 { return c - 87 } }
91 if c >= 65 { if c <= 70 { return c - 55 } }
92 return 0 - 1
93}
94func gc_hexd(src: *u8, n: i64, dst: *u8) -> i64 {
95 if n % 2 != 0 { return 0 - 1 }
96 var i: i64 = 0
97 while i < n {
98 let h: i64 = gc_hexv(src[i] as i64)
99 let l: i64 = gc_hexv(src[i+1] as i64)
100 if h < 0 { return 0 - 1 }
101 if l < 0 { return 0 - 1 }
102 dst[i/2] = (h * 16 + l) as u8
103 i = i + 2
104 }
105 return n / 2
106}
107// generic conf value: <key>| at line start (same shape as C1/C2 -- lib extraction owed, named in plan)
108func gc_confs(confpath: *u8, key: *u8, out: *u8, cap: i64) -> i64 {
109 let ol: *i64 = sts_mm(16) as *i64
110 let b: *u8 = sys_read_file(confpath, ol)
111 let n: i64 = ol[0]
112 if b as i64 == 0 { return 0 - 1 }
113 if n <= 0 { return 0 - 1 }
114 let kl: i64 = gc_len(key)
115 var i: i64 = 0
116 while i < n {
117 var j: i64 = 0
118 var hit: i64 = 1
119 while j < kl {
120 if i + j >= n { hit = 0; break }
121 if b[i+j] != key[j] { hit = 0; break }
122 j = j + 1
123 }
124 if hit == 1 {
125 if i + kl >= n { hit = 0 } else { if b[i+kl] != (GC_PIPE as u8) { hit = 0 } }
126 }
127 if hit == 1 {
128 var v: i64 = i + kl + 1
129 var o: i64 = 0
130 while v < n {
131 if b[v] == (GC_NL as u8) { break }
132 if o < cap - 1 { out[o] = b[v]; o = o + 1 }
133 v = v + 1
134 }
135 out[o] = 0 as u8
136 return o
137 }
138 while i < n { if b[i] == (GC_NL as u8) { break } i = i + 1 }
139 i = i + 1
140 }
141 return 0 - 1
142}
143// game row lookup: game|<id>|cols|rows|winlen|mode -> fills g[0]=cols g[1]=rows g[2]=winlen g[3]=mode
144func gc_game(gconf: *u8, id: *u8, g: *i64) -> i64 {
145 let ol: *i64 = sts_mm(16) as *i64
146 let b: *u8 = sys_read_file(gconf, ol)
147 let n: i64 = ol[0]
148 if b as i64 == 0 { return 0 - 1 }
149 if n <= 0 { return 0 - 1 }
150 let idl: i64 = gc_len(id)
151 var i: i64 = 0
152 while i < n {
153 var hit: i64 = 1
154 let pre: *u8 = "game|" as *u8
155 var j: i64 = 0
156 while j < 5 {
157 if i + j >= n { hit = 0; break }
158 if b[i+j] != pre[j] { hit = 0; break }
159 j = j + 1
160 }
161 if hit == 1 {
162 var k: i64 = 0
163 while k < idl {
164 if i + 5 + k >= n { hit = 0; break }
165 if b[i+5+k] != id[k] { hit = 0; break }
166 k = k + 1
167 }
168 if hit == 1 { if i + 5 + idl >= n { hit = 0 } else { if b[i+5+idl] != (GC_PIPE as u8) { hit = 0 } } }
169 }
170 if hit == 1 {
171 var p: i64 = i + 5 + idl + 1
172 var f: i64 = 0
173 var acc: i64 = 0
174 var seen: i64 = 0
175 g[3] = 0
176 while p < n {
177 let c: i64 = b[p] as i64
178 if c == GC_NL { break }
179 if c == GC_PIPE {
180 if f < 3 { g[f] = acc }
181 f = f + 1
182 acc = 0
183 seen = 0
184 } else {
185 if c >= 48 { if c <= 57 { acc = acc * 10 + (c - 48); seen = 1 } }
186 if f == 3 { if g[3] == 0 {
187 // FIRST letter only decides (drop / place). BUG THE GATE CAUGHT 2026-08-16:
188 // scanning every byte read the trailing p of the word drop as place-mode --
189 // one letter, five red teeth. A dispatch on a word must anchor, never scan.
190 if c == 100 { g[3] = GC_MODE_DROP }
191 if c == 112 { g[3] = GC_MODE_PLACE }
192 } }
193 }
194 p = p + 1
195 }
196 if f < 3 { if seen == 1 { g[f] = acc } }
197 if g[0] < 1 { return 0 - 2 }
198 if g[1] < 1 { return 0 - 2 }
199 if g[2] < 1 { return 0 - 2 }
200 if g[3] == 0 { return 0 - 2 }
201 if g[0] * g[1] > GC_BOARD_MAX { return 0 - 3 }
202 return 1
203 }
204 while i < n { if b[i] == (GC_NL as u8) { break } i = i + 1 }
205 i = i + 1
206 }
207 return 0 - 1
208}
209func gc_prefix(chatconf: *u8, room: *u8, out: *u8) -> i64 {
210 if gc_tok_ok(room) == 0 { return 0 - 1 }
211 let rl: i64 = gc_confs(chatconf, "store_root" as *u8, out, GC_PFX_CAP - GC_NAME_MAX - 8)
212 if rl <= 0 { return 0 - 2 }
213 var o: i64 = rl
214 o = ss_cat(out, o, "chat_" as *u8)
215 o = ss_cat(out, o, room)
216 o = ss_cat(out, o, "-" as *u8)
217 out[o] = 0 as u8
218 return o
219}
220// nth pipe-separated field of a row value; returns length, fills op with pointer offset
221func gc_field(p: *u8, l: i64, idx: i64, op: *i64) -> i64 {
222 var f: i64 = 0
223 var i: i64 = 0
224 var st: i64 = 0
225 while i < l {
226 if f == idx { st = i; break }
227 if p[i] == (GC_PIPE as u8) { f = f + 1 }
228 i = i + 1
229 }
230 if f != idx { return 0 - 1 }
231 var e: i64 = st
232 while e < l { if p[e] == (GC_PIPE as u8) { break } e = e + 1 }
233 op[0] = st
234 return e - st
235}
236// fork the C1 elf: argv already assembled; rc returned, output in out
237func gc_chat(chatelf: *u8, av: *i64, out: *u8, ol: *i64) -> i64 {
238 return tr_run_capture(chatelf, av, out, GC_OUT_CAP, ol)
239}
240
241// REPLAY -- the referee. Reads the room plane directly, replays every move row in order, validating
242// each against the reconstructed state; invalid rows are COUNTED and ignored, identically for every
243// reader. Fills st: [0]=cols [1]=rows [2]=winlen [3]=mode [4]=accepted [5]=rejected [6]=chat_rows
244// [7]=chat_hidden [8]=state [9]=winner(1/2) [10]=next_token(1/2) [11]=rows_total [12]=last_epoch
245// p1/p2 name bufs filled from the game-start row. board = cols*rows bytes (0/1/2).
246func gc_replay(pfx: *u8, gconf: *u8, board: *u8, st: *i64, p1: *u8, p2: *u8, gid: *u8) -> i64 {
247 let pq: *i64 = sts_mm(16) as *i64
248 let lq: *i64 = sts_mm(16) as *i64
249 if ss_get(pfx, "q:n" as *u8, pq, lq) != 1 { return 0 - 1 } // room absent
250 let n: i64 = sts_atoi(pq[0] as *u8, lq[0])
251 st[11] = n
252 if n < 2 { return 0 - 2 } // no game-start row
253 let h: *i64 = ss_open_cached(pfx)
254 if h as i64 == 0 { return 0 - 3 }
255 let key: *u8 = sts_mm(64)
256 let dk: *u8 = sts_mm(64)
257 let dp: *i64 = sts_mm(16) as *i64
258 let dl: *i64 = sts_mm(16) as *i64
259 let fo: *i64 = sts_mm(16) as *i64
260 let body: *u8 = sts_mm(GC_OUT_CAP)
261 st[4] = 0
262 st[5] = 0
263 st[6] = 0
264 st[7] = 0
265 st[8] = GC_ST_ONGOING
266 st[9] = 0
267 st[12] = 0
268 var have_game: i64 = 0
269 var i: i64 = 0
270 while i < n {
271 sts_rowkey(i, key)
272 if ss_hget(h, key, pq, lq) != 1 { return 0 - 4 }
273 let rv: *u8 = pq[0] as *u8
274 let rl: i64 = lq[0]
275 let el: i64 = gc_field(rv, rl, 2, fo)
276 if el > 0 { st[12] = gc_atoi((rv as i64 + fo[0]) as *u8, el) }
277 let kl2: i64 = gc_field(rv, rl, 3, fo)
278 var kind: i64 = 0
279 if kl2 > 0 { kind = gc_atoi((rv as i64 + fo[0]) as *u8, kl2) }
280 let sl: i64 = gc_field(rv, rl, 4, fo)
281 let so: i64 = fo[0]
282 let hl: i64 = gc_field(rv, rl, 5, fo)
283 var bl: i64 = 0
284 if hl > 0 { bl = gc_hexd((rv as i64 + fo[0]) as *u8, hl, body) }
285 if bl < 0 { bl = 0 }
286 body[bl] = 0 as u8
287 if i == 0 { i = i + 1 } else {
288 if have_game == 0 {
289 // row index 1 must be the game-start row: kind 4, body game:<id>:<p1>:<p2>
290 var isg: i64 = 0
291 if kind == 4 { if bl > 5 {
292 let gp: *u8 = "game:" as *u8
293 var m2: i64 = 1
294 var q2: i64 = 0
295 while q2 < 5 { if body[q2] != gp[q2] { m2 = 0; q2 = 5 } else { q2 = q2 + 1 } }
296 isg = m2
297 } }
298 if isg == 0 { return 0 - 5 }
299 // split body game:<id>:<p1>:<p2> on ':'
300 var c1: i64 = 5
301 var seg: i64 = 0
302 var w2: i64 = 0
303 while c1 <= bl {
304 var ch: i64 = 0
305 if c1 < bl { ch = body[c1] as i64 }
306 if c1 == bl { ch = 58 }
307 if ch == 58 {
308 if seg == 0 { gid[w2] = 0 as u8 }
309 if seg == 1 { p1[w2] = 0 as u8 }
310 if seg == 2 { p2[w2] = 0 as u8 }
311 seg = seg + 1
312 w2 = 0
313 } else {
314 if w2 < GC_NAME_MAX {
315 if seg == 0 { gid[w2] = ch as u8 }
316 if seg == 1 { p1[w2] = ch as u8 }
317 if seg == 2 { p2[w2] = ch as u8 }
318 w2 = w2 + 1
319 }
320 }
321 c1 = c1 + 1
322 }
323 if seg < 3 { return 0 - 5 }
324 let g: *i64 = sts_mm(64) as *i64
325 let gr: i64 = gc_game(gconf, gid, g)
326 if gr != 1 { return 0 - 6 }
327 st[0] = g[0]
328 st[1] = g[1]
329 st[2] = g[2]
330 st[3] = g[3]
331 var z: i64 = 0
332 while z < g[0] * g[1] { board[z] = 0 as u8; z = z + 1 }
333 have_game = 1
334 i = i + 1
335 } else {
336 // a move row is kind 1 with body mv:<n>
337 var ismv: i64 = 0
338 if kind == 1 { if bl > 3 {
339 if body[0] == (109 as u8) { if body[1] == (118 as u8) { if body[2] == (58 as u8) { ismv = 1 } } }
340 } }
341 if ismv == 1 {
342 var okmv: i64 = 0
343 if st[8] == GC_ST_ONGOING {
344 // sender must be the player whose turn it is
345 let want: i64 = (st[4] % 2) + 1
346 var wp: *u8 = p1
347 if want == 2 { wp = p2 }
348 var senok: i64 = 1
349 let wl: i64 = gc_len(wp)
350 if wl != sl { senok = 0 } else {
351 var q3: i64 = 0
352 while q3 < wl { if rv[so+q3] != wp[q3] { senok = 0; q3 = wl } else { q3 = q3 + 1 } }
353 }
354 if senok == 1 {
355 let mvn: i64 = gc_atoi((body as i64 + 3) as *u8, bl - 3)
356 var cell: i64 = 0 - 1
357 if st[3] == GC_MODE_DROP {
358 if mvn >= 0 { if mvn < st[0] {
359 var r2: i64 = 0
360 while r2 < st[1] {
361 if board[r2 * st[0] + mvn] == (0 as u8) { cell = r2 * st[0] + mvn; break }
362 r2 = r2 + 1
363 }
364 } }
365 } else {
366 if mvn >= 0 { if mvn < st[0] * st[1] { if board[mvn] == (0 as u8) { cell = mvn } } }
367 }
368 if cell >= 0 {
369 board[cell] = want as u8
370 st[4] = st[4] + 1
371 okmv = 1
372 // win scan from the placed cell: 4 directions, both ways
373 let cc: i64 = cell % st[0]
374 let cr: i64 = cell / st[0]
375 var d: i64 = 0
376 while d < 4 {
377 var dx: i64 = 1
378 var dy: i64 = 0
379 if d == 1 { dx = 0; dy = 1 }
380 if d == 2 { dx = 1; dy = 1 }
381 if d == 3 { dx = 1; dy = 0 - 1 }
382 var cnt: i64 = 1
383 var sgn: i64 = 0 - 1
384 while sgn <= 1 {
385 if sgn != 0 {
386 var sx: i64 = cc + dx * sgn
387 var sy: i64 = cr + dy * sgn
388 var go: i64 = 1
389 while go == 1 {
390 if sx < 0 { go = 0 } else { if sx >= st[0] { go = 0 } else { if sy < 0 { go = 0 } else { if sy >= st[1] { go = 0 } else {
391 if board[sy * st[0] + sx] == (want as u8) { cnt = cnt + 1; sx = sx + dx * sgn; sy = sy + dy * sgn } else { go = 0 }
392 } } } }
393 }
394 }
395 sgn = sgn + 2
396 }
397 if cnt >= st[2] { st[8] = GC_ST_WIN; st[9] = want; d = 4 } else { d = d + 1 }
398 }
399 if st[8] == GC_ST_ONGOING { if st[4] >= st[0] * st[1] { st[8] = GC_ST_DRAW } }
400 }
401 }
402 }
403 if okmv == 0 { st[5] = st[5] + 1 }
404 } else {
405 st[6] = st[6] + 1
406 var dl3: i64 = 0
407 dl3 = ss_cat(dk, 0, "del:" as *u8)
408 dl3 = ss_catn(dk, dl3, i + 1)
409 dk[dl3] = 0 as u8
410 if ss_hget(h, dk, dp, dl) == 1 { st[7] = st[7] + 1 }
411 }
412 i = i + 1
413 } }
414 }
415 if have_game == 0 { return 0 - 5 }
416 st[10] = (st[4] % 2) + 1
417 return 1
418}
419func gc_replay_err(rr: i64) -> i64 {
420 if rr == (0 - 1) { gc_w("GAME-REFUSED room-absent -- start a session first: nx_game_chat start <game> <session> <p1> <p2>\n" as *u8) return GC_EXIT_REFUSED }
421 if rr == (0 - 2) { gc_w("GAME-REFUSED not-a-game-room -- no game-start row\n" as *u8) return GC_EXIT_REFUSED }
422 if rr == (0 - 3) { gc_w("GAME-RETRY open-failed\n" as *u8) return GC_EXIT_RETRY }
423 if rr == (0 - 4) { gc_w("GAME-CORRUPT row-gap\n" as *u8) return GC_EXIT_CORRUPT }
424 if rr == (0 - 5) { gc_w("GAME-REFUSED not-a-game-room -- row 2 is not a game-start row\n" as *u8) return GC_EXIT_REFUSED }
425 if rr == (0 - 6) { gc_w("GAME-REFUSED unknown-game -- the id in the game-start row has no games.conf row (or its board exceeds the sanity bound)\n" as *u8) return GC_EXIT_REFUSED }
426 return GC_EXIT_CORRUPT
427}
428
429// THE C12 CONTRACT SYMBOL: open a game session room and stamp the game-start row.
430func gc_room_wire(game: *u8, session: *u8, p1: *u8, p2: *u8, gconf: *u8) -> i64 {
431 if gc_tok_ok(game) == 0 { gc_w("GAME-REFUSED bad-token game\n" as *u8) return GC_EXIT_REFUSED }
432 if gc_tok_ok(session) == 0 { gc_w("GAME-REFUSED bad-token session\n" as *u8) return GC_EXIT_REFUSED }
433 if gc_tok_ok(p1) == 0 { gc_w("GAME-REFUSED bad-token p1\n" as *u8) return GC_EXIT_REFUSED }
434 if gc_tok_ok(p2) == 0 { gc_w("GAME-REFUSED bad-token p2\n" as *u8) return GC_EXIT_REFUSED }
435 if gc_eq(p1, p2) == 1 { gc_w("GAME-REFUSED same-player-twice\n" as *u8) return GC_EXIT_REFUSED }
436 let g: *i64 = sts_mm(64) as *i64
437 let gr: i64 = gc_game(gconf, game, g)
438 if gr == (0 - 3) { gc_w("GAME-REFUSED board-too-big -- cols*rows exceeds the sanity bound 4096; fix games.conf\n" as *u8) return GC_EXIT_REFUSED }
439 if gr != 1 { gc_w("GAME-REFUSED unknown-game id=" as *u8) gc_w(game) gc_w(" conf=" as *u8) gc_w(gconf) gc_w("\n" as *u8) return GC_EXIT_REFUSED }
440 let chatelf: *u8 = sts_mm(GC_CONF_CAP)
441 if gc_confs(gconf, "chat_elf" as *u8, chatelf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_elf\n" as *u8) return GC_EXIT_REFUSED }
442 let chatconf: *u8 = sts_mm(GC_CONF_CAP)
443 if gc_confs(gconf, "chat_conf" as *u8, chatconf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_conf\n" as *u8) return GC_EXIT_REFUSED }
444 let room: *u8 = sts_mm(GC_NAME_MAX * 3)
445 var o: i64 = ss_cat(room, 0, "g_" as *u8)
446 o = ss_cat(room, o, game)
447 o = ss_cat(room, o, "_" as *u8)
448 o = ss_cat(room, o, session)
449 room[o] = 0 as u8
450 if gc_tok_ok(room) == 0 { gc_w("GAME-REFUSED room-name-too-long\n" as *u8) return GC_EXIT_REFUSED }
451 let out: *u8 = sts_mm(GC_OUT_CAP)
452 let ol: *i64 = sts_mm(16) as *i64
453 let av: *i64 = sts_mm(128) as *i64
454 av[0] = chatelf as i64
455 av[1] = "open" as i64
456 av[2] = room as i64
457 av[3] = p1 as i64
458 av[4] = chatconf as i64
459 av[5] = 0
460 let rc1: i64 = gc_chat(chatelf, av, out, ol)
461 if rc1 != 0 { gc_w("GAME-REFUSED session-exists-or-open-failed room=" as *u8) gc_w(room) gc_w(" (open is create-only)\n" as *u8) return GC_EXIT_REFUSED }
462 let body: *u8 = sts_mm(GC_NAME_MAX * 4)
463 o = ss_cat(body, 0, "game:" as *u8)
464 o = ss_cat(body, o, game)
465 o = ss_cat(body, o, ":" as *u8)
466 o = ss_cat(body, o, p1)
467 o = ss_cat(body, o, ":" as *u8)
468 o = ss_cat(body, o, p2)
469 body[o] = 0 as u8
470 av[1] = "append" as i64
471 av[3] = p1 as i64
472 av[4] = "4" as i64
473 av[5] = body as i64
474 av[6] = chatconf as i64
475 av[7] = 0
476 let rc2: i64 = gc_chat(chatelf, av, out, ol)
477 if rc2 != 0 { gc_w("GAME-RED game-start-append-failed\n" as *u8) return GC_EXIT_CORRUPT }
478 gc_w("GAME-START room=" as *u8)
479 gc_w(room)
480 gc_w(" game=" as *u8)
481 gc_w(game)
482 gc_w(" cols=" as *u8)
483 gc_n(g[0])
484 gc_w(" rows=" as *u8)
485 gc_n(g[1])
486 gc_w(" winlen=" as *u8)
487 gc_n(g[2])
488 gc_w(" p1=" as *u8)
489 gc_w(p1)
490 gc_w(" p2=" as *u8)
491 gc_w(p2)
492 gc_w(" turn=" as *u8)
493 gc_w(p1)
494 gc_w("\n" as *u8)
495 return GC_EXIT_OK
496}
497
498func gc_move(room: *u8, player: *u8, mv: *u8, gconf: *u8) -> i64 {
499 let chatelf: *u8 = sts_mm(GC_CONF_CAP)
500 if gc_confs(gconf, "chat_elf" as *u8, chatelf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_elf\n" as *u8) return GC_EXIT_REFUSED }
501 let chatconf: *u8 = sts_mm(GC_CONF_CAP)
502 if gc_confs(gconf, "chat_conf" as *u8, chatconf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_conf\n" as *u8) return GC_EXIT_REFUSED }
503 let pfx: *u8 = sts_mm(GC_PFX_CAP)
504 if gc_prefix(chatconf, room, pfx) < 0 { gc_w("GAME-REFUSED bad-token room\n" as *u8) return GC_EXIT_REFUSED }
505 let board: *u8 = sts_mm(GC_BOARD_MAX)
506 let st: *i64 = sts_mm(128) as *i64
507 let p1: *u8 = sts_mm(GC_NAME_MAX + 4)
508 let p2: *u8 = sts_mm(GC_NAME_MAX + 4)
509 let gid: *u8 = sts_mm(GC_NAME_MAX + 4)
510 let rr: i64 = gc_replay(pfx, gconf, board, st, p1, p2, gid)
511 if rr != 1 { return gc_replay_err(rr) }
512 if st[8] == GC_ST_WIN {
513 gc_w("GAME-REFUSED game-over state=win winner=" as *u8)
514 if st[9] == 1 { gc_w(p1) } else { gc_w(p2) }
515 gc_w("\n" as *u8)
516 return GC_EXIT_REFUSED
517 }
518 if st[8] == GC_ST_DRAW { gc_w("GAME-REFUSED game-over state=draw\n" as *u8) return GC_EXIT_REFUSED }
519 var expect: *u8 = p1
520 if st[10] == 2 { expect = p2 }
521 if gc_eq(player, expect) == 0 {
522 gc_w("GAME-REFUSED out-of-turn expected=" as *u8)
523 gc_w(expect)
524 gc_w(" got=" as *u8)
525 gc_w(player)
526 gc_w("\n" as *u8)
527 return GC_EXIT_REFUSED
528 }
529 let mvn: i64 = gc_atoi(mv, gc_len(mv))
530 if st[3] == GC_MODE_DROP {
531 if mvn < 0 { gc_w("GAME-REFUSED illegal-move\n" as *u8) return GC_EXIT_REFUSED }
532 if mvn >= st[0] {
533 gc_w("GAME-REFUSED illegal-move col=" as *u8)
534 gc_n(mvn)
535 gc_w(" cols=" as *u8)
536 gc_n(st[0])
537 gc_w("\n" as *u8)
538 return GC_EXIT_REFUSED
539 }
540 var full: i64 = 1
541 var r3: i64 = 0
542 while r3 < st[1] { if board[r3 * st[0] + mvn] == (0 as u8) { full = 0; break } r3 = r3 + 1 }
543 if full == 1 {
544 gc_w("GAME-REFUSED column-full col=" as *u8)
545 gc_n(mvn)
546 gc_w("\n" as *u8)
547 return GC_EXIT_REFUSED
548 }
549 } else {
550 if mvn < 0 { gc_w("GAME-REFUSED illegal-move\n" as *u8) return GC_EXIT_REFUSED }
551 if mvn >= st[0] * st[1] { gc_w("GAME-REFUSED illegal-move cell out of range\n" as *u8) return GC_EXIT_REFUSED }
552 if board[mvn] != (0 as u8) { gc_w("GAME-REFUSED cell-occupied cell=" as *u8) gc_n(mvn) gc_w("\n" as *u8) return GC_EXIT_REFUSED }
553 }
554 let body: *u8 = sts_mm(64)
555 var o: i64 = ss_cat(body, 0, "mv:" as *u8)
556 o = ss_catn(body, o, mvn)
557 body[o] = 0 as u8
558 let out: *u8 = sts_mm(GC_OUT_CAP)
559 let ol: *i64 = sts_mm(16) as *i64
560 let av: *i64 = sts_mm(128) as *i64
561 av[0] = chatelf as i64
562 av[1] = "append" as i64
563 av[2] = room as i64
564 av[3] = player as i64
565 av[4] = "1" as i64
566 av[5] = body as i64
567 av[6] = chatconf as i64
568 av[7] = 0
569 let rc: i64 = gc_chat(chatelf, av, out, ol)
570 if rc != 0 { gc_w("GAME-RED move-append-failed (see C1 refusal above)\n" as *u8) return GC_EXIT_CORRUPT }
571 // replay again to report the post-move truth (the log is the state)
572 let rr2: i64 = gc_replay(pfx, gconf, board, st, p1, p2, gid)
573 if rr2 != 1 { return gc_replay_err(rr2) }
574 gc_w("GAME-MOVE-OK room=" as *u8)
575 gc_w(room)
576 gc_w(" player=" as *u8)
577 gc_w(player)
578 gc_w(" move=" as *u8)
579 gc_n(mvn)
580 gc_w(" state=" as *u8)
581 if st[8] == GC_ST_WIN { gc_w("win winner=" as *u8) if st[9] == 1 { gc_w(p1) } else { gc_w(p2) } } else {
582 if st[8] == GC_ST_DRAW { gc_w("draw" as *u8) } else {
583 gc_w("ongoing turn_next=" as *u8)
584 if st[10] == 1 { gc_w(p1) } else { gc_w(p2) }
585 }
586 }
587 gc_w("\n" as *u8)
588 return GC_EXIT_OK
589}
590
591func gc_say(room: *u8, player: *u8, text: *u8, gconf: *u8) -> i64 {
592 if gc_len(text) > 2 {
593 if text[0] == (109 as u8) { if text[1] == (118 as u8) { if text[2] == (58 as u8) {
594 gc_w("GAME-REFUSED reserved-prefix -- chat may not start with mv: (that is the move wire)\n" as *u8)
595 return GC_EXIT_REFUSED
596 } } }
597 }
598 let chatelf: *u8 = sts_mm(GC_CONF_CAP)
599 if gc_confs(gconf, "chat_elf" as *u8, chatelf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_elf\n" as *u8) return GC_EXIT_REFUSED }
600 let chatconf: *u8 = sts_mm(GC_CONF_CAP)
601 if gc_confs(gconf, "chat_conf" as *u8, chatconf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_conf\n" as *u8) return GC_EXIT_REFUSED }
602 let out: *u8 = sts_mm(GC_OUT_CAP)
603 let ol: *i64 = sts_mm(16) as *i64
604 let av: *i64 = sts_mm(128) as *i64
605 av[0] = chatelf as i64
606 av[1] = "append" as i64
607 av[2] = room as i64
608 av[3] = player as i64
609 av[4] = "1" as i64
610 av[5] = text as i64
611 av[6] = chatconf as i64
612 av[7] = 0
613 let rc: i64 = gc_chat(chatelf, av, out, ol)
614 if rc != 0 { gc_w("GAME-REFUSED chat-append-failed (see C1 refusal above)\n" as *u8) return GC_EXIT_REFUSED }
615 gc_w("GAME-CHAT-OK room=" as *u8)
616 gc_w(room)
617 gc_w(" player=" as *u8)
618 gc_w(player)
619 gc_w("\n" as *u8)
620 return GC_EXIT_OK
621}
622
623func gc_board_print(board: *u8, st: *i64) -> i64 {
624 var r: i64 = st[1] - 1
625 while r >= 0 {
626 var c: i64 = 0
627 let line: *u8 = sts_mm(st[0] + 2)
628 while c < st[0] {
629 let v: i64 = board[r * st[0] + c] as i64
630 if v == 0 { line[c] = 46 as u8 }
631 if v == 1 { line[c] = 65 as u8 }
632 if v == 2 { line[c] = 66 as u8 }
633 c = c + 1
634 }
635 line[st[0]] = GC_NL as u8
636 sys_write(1, line, st[0] + 1)
637 r = r - 1
638 }
639 return 0
640}
641func gc_board(room: *u8, gconf: *u8) -> i64 {
642 let chatconf: *u8 = sts_mm(GC_CONF_CAP)
643 if gc_confs(gconf, "chat_conf" as *u8, chatconf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_conf\n" as *u8) return GC_EXIT_REFUSED }
644 let pfx: *u8 = sts_mm(GC_PFX_CAP)
645 if gc_prefix(chatconf, room, pfx) < 0 { gc_w("GAME-REFUSED bad-token room\n" as *u8) return GC_EXIT_REFUSED }
646 let board: *u8 = sts_mm(GC_BOARD_MAX)
647 let st: *i64 = sts_mm(128) as *i64
648 let p1: *u8 = sts_mm(GC_NAME_MAX + 4)
649 let p2: *u8 = sts_mm(GC_NAME_MAX + 4)
650 let gid: *u8 = sts_mm(GC_NAME_MAX + 4)
651 let rr: i64 = gc_replay(pfx, gconf, board, st, p1, p2, gid)
652 if rr != 1 { return gc_replay_err(rr) }
653 gc_w("GAME-BOARD room=" as *u8)
654 gc_w(room)
655 gc_w(" game=" as *u8)
656 gc_w(gid)
657 gc_w(" p1=" as *u8)
658 gc_w(p1)
659 gc_w(" p2=" as *u8)
660 gc_w(p2)
661 gc_w(" state=" as *u8)
662 if st[8] == GC_ST_WIN { gc_w("win winner=" as *u8) if st[9] == 1 { gc_w(p1) } else { gc_w(p2) } } else {
663 if st[8] == GC_ST_DRAW { gc_w("draw" as *u8) } else { gc_w("ongoing turn_next=" as *u8) if st[10] == 1 { gc_w(p1) } else { gc_w(p2) } }
664 }
665 gc_w("\n" as *u8)
666 gc_board_print(board, st)
667 let scanned: i64 = st[11] - 2
668 gc_w("partition rows_total=" as *u8)
669 gc_n(st[11])
670 gc_w(" open_row=1 game_start=1 moves_applied=" as *u8)
671 gc_n(st[4])
672 gc_w(" moves_rejected=" as *u8)
673 gc_n(st[5])
674 gc_w(" chat_rows=" as *u8)
675 gc_n(st[6])
676 gc_w(" chat_hidden=" as *u8)
677 gc_n(st[7])
678 gc_w(" " as *u8)
679 if st[4] + st[5] + st[6] == scanned { gc_w("sums=ok\n" as *u8) } else { gc_w("sums=BROKEN\n" as *u8) return GC_EXIT_CORRUPT }
680 return GC_EXIT_OK
681}
682
683// deterministic HTML artifact: stamped from the LAST ROW's epoch, never the wall clock.
684func gc_emit(room: *u8, outpath: *u8, gconf: *u8) -> i64 {
685 let chatconf: *u8 = sts_mm(GC_CONF_CAP)
686 if gc_confs(gconf, "chat_conf" as *u8, chatconf, GC_CONF_CAP) <= 0 { gc_w("GAME-REFUSED conf-missing row=chat_conf\n" as *u8) return GC_EXIT_REFUSED }
687 let pfx: *u8 = sts_mm(GC_PFX_CAP)
688 if gc_prefix(chatconf, room, pfx) < 0 { gc_w("GAME-REFUSED bad-token room\n" as *u8) return GC_EXIT_REFUSED }
689 let board: *u8 = sts_mm(GC_BOARD_MAX)
690 let st: *i64 = sts_mm(128) as *i64
691 let p1: *u8 = sts_mm(GC_NAME_MAX + 4)
692 let p2: *u8 = sts_mm(GC_NAME_MAX + 4)
693 let gid: *u8 = sts_mm(GC_NAME_MAX + 4)
694 let rr: i64 = gc_replay(pfx, gconf, board, st, p1, p2, gid)
695 if rr != 1 { return gc_replay_err(rr) }
696 let hb: *u8 = sts_mm(GC_HTML_CAP)
697 var o: i64 = 0
698 // doctype assembled to keep the bang out of string literals (nx_cc lexer law)
699 o = ss_cat(hb, o, "<" as *u8)
700 hb[o] = 33 as u8
701 o = o + 1
702 o = ss_cat(hb, o, "doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><meta name='generator' content='nx_game_chat'><title>Nishi Live Match</title><style>body{margin:0;background:rgb(11,15,20);color:rgb(230,237,243);font:16px/1.5 system-ui,sans-serif}main{max-width:760px;margin:0 auto;padding:24px 16px}h1{font-size:1.4rem;letter-spacing:-.02em}table{border-collapse:collapse;margin:16px 0}td{width:44px;height:44px;border:1px solid rgb(36,49,64);border-radius:6px}p,li{color:rgb(138,160,180)}.a{background:rgb(248,81,73)}.b{background:rgb(245,197,24)}.e{background:rgb(14,23,34)}.st{color:rgb(76,194,255);font-weight:600}code{color:rgb(76,194,255)}</style></head><body><main>" as *u8)
703 o = ss_cat(hb, o, "<h1>Nishi Live Match -- " as *u8)
704 o = ss_cat(hb, o, gid)
705 o = ss_cat(hb, o, "</h1><p><b>" as *u8)
706 o = ss_cat(hb, o, p1)
707 o = ss_cat(hb, o, "</b> (red) vs <b>" as *u8)
708 o = ss_cat(hb, o, p2)
709 o = ss_cat(hb, o, "</b> (gold) -- <span class='st'>" as *u8)
710 if st[8] == GC_ST_WIN {
711 o = ss_cat(hb, o, "winner: " as *u8)
712 if st[9] == 1 { o = ss_cat(hb, o, p1) } else { o = ss_cat(hb, o, p2) }
713 } else {
714 if st[8] == GC_ST_DRAW { o = ss_cat(hb, o, "draw" as *u8) } else {
715 o = ss_cat(hb, o, "ongoing, next: " as *u8)
716 if st[10] == 1 { o = ss_cat(hb, o, p1) } else { o = ss_cat(hb, o, p2) }
717 }
718 }
719 o = ss_cat(hb, o, "</span></p><table>" as *u8)
720 var r: i64 = st[1] - 1
721 while r >= 0 {
722 o = ss_cat(hb, o, "<tr>" as *u8)
723 var c: i64 = 0
724 while c < st[0] {
725 let v: i64 = board[r * st[0] + c] as i64
726 if v == 1 { o = ss_cat(hb, o, "<td class='a'></td>" as *u8) }
727 if v == 2 { o = ss_cat(hb, o, "<td class='b'></td>" as *u8) }
728 if v == 0 { o = ss_cat(hb, o, "<td class='e'></td>" as *u8) }
729 c = c + 1
730 }
731 o = ss_cat(hb, o, "</tr>" as *u8)
732 r = r - 1
733 }
734 o = ss_cat(hb, o, "</table><p>moves applied " as *u8)
735 o = ss_catn(hb, o, st[4])
736 o = ss_cat(hb, o, " -- replay-rejected " as *u8)
737 o = ss_catn(hb, o, st[5])
738 o = ss_cat(hb, o, " -- chat messages " as *u8)
739 o = ss_catn(hb, o, st[6])
740 o = ss_cat(hb, o, "</p><p>This match was played THROUGH the sovereign message store: every move is a row in room <code>" as *u8)
741 o = ss_cat(hb, o, room)
742 o = ss_cat(hb, o, "</code>, the board is a deterministic replay of that log, and the same replay runs on every device -- no game server, no middleman, family hardware only. Log epoch " as *u8)
743 o = ss_catn(hb, o, st[12])
744 o = ss_cat(hb, o, ".</p></main></body></html>" as *u8)
745 // NX-DERIVED marker: a generated artifact must declare itself
746 o = ss_cat(hb, o, "\n" as *u8)
747 let fd: i64 = sys_openat_wr(outpath, 420)
748 if fd < 0 { gc_w("GAME-RED emit-open-failed path=" as *u8) gc_w(outpath) gc_w("\n" as *u8) return GC_EXIT_CORRUPT }
749 var w3: i64 = 0
750 while w3 < o { let ww: i64 = sys_write(fd, (hb as i64 + w3) as *u8, o - w3); if ww <= 0 { break } w3 = w3 + ww }
751 sys_fsync(fd)
752 sys_close(fd)
753 gc_w("GAME-EMIT-OK room=" as *u8)
754 gc_w(room)
755 gc_w(" path=" as *u8)
756 gc_w(outpath)
757 gc_w(" bytes=" as *u8)
758 gc_n(o)
759 gc_w(" stamp_epoch=" as *u8)
760 gc_n(st[12])
761 gc_w("\n" as *u8)
762 return GC_EXIT_OK
763}
764
765func gc_usage() -> i64 {
766 gc_w("usage: nx_game_chat start <game> <session> <p1> <p2> [gamesconf] | move <room> <player> <n> [gamesconf] | say <room> <player> <text> [gamesconf] | board <room> [gamesconf] | emit <room> <outpath> [gamesconf]\n" as *u8)
767 return GC_EXIT_USAGE
768}
769func gc_confpick(argc: i64, argv: *i64, idx: i64) -> *u8 {
770 if argc > idx { return argv[idx] as *u8 }
771 return "knowledge/comms/games.conf" as *u8
772}
773func main(argc: i64, argv: *i64) -> i64 {
774 if argc < 3 { let u: i64 = gc_usage() sys_exit(u) return u }
775 let verb: *u8 = argv[1] as *u8
776 if gc_eq(verb, "start" as *u8) == 1 {
777 if argc < 6 { let u: i64 = gc_usage() sys_exit(u) return u }
778 let rc: i64 = gc_room_wire(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, gc_confpick(argc, argv, 6))
779 sys_exit(rc)
780 return rc
781 }
782 if gc_eq(verb, "move" as *u8) == 1 {
783 if argc < 5 { let u: i64 = gc_usage() sys_exit(u) return u }
784 let rc: i64 = gc_move(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, gc_confpick(argc, argv, 5))
785 sys_exit(rc)
786 return rc
787 }
788 if gc_eq(verb, "say" as *u8) == 1 {
789 if argc < 5 { let u: i64 = gc_usage() sys_exit(u) return u }
790 let rc: i64 = gc_say(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, gc_confpick(argc, argv, 5))
791 sys_exit(rc)
792 return rc
793 }
794 if gc_eq(verb, "board" as *u8) == 1 {
795 let rc: i64 = gc_board(argv[2] as *u8, gc_confpick(argc, argv, 3))
796 sys_exit(rc)
797 return rc
798 }
799 if gc_eq(verb, "emit" as *u8) == 1 {
800 if argc < 4 { let u: i64 = gc_usage() sys_exit(u) return u }
801 let rc: i64 = gc_emit(argv[2] as *u8, argv[3] as *u8, gc_confpick(argc, argv, 4))
802 sys_exit(rc)
803 return rc
804 }
805 let u2: i64 = gc_usage()
806 sys_exit(u2)
807 return u2
808}