code wiki / (root) / nx_game_chat.nx

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}