code wiki / (root) / nx_games_page.nx

nx_games_page.nx source

↩ module page · 356 lines · 30974 B

1// nx_games_page.nx -- EMIT the live nishifamily.com/games portal page. 2// Classic abstract/board/puzzle games built from scratch (rules are not copyrightable -> clean 3// original implementations, no ROMs rehosted, no third-party code). Client behaviour and the shared 4// emit helpers live in nx_games_js.nx; this organ authors document structure, tokens and copy. 5// license_tier: ORIGINAL 6// 7// genealogy_id: nishi_games_portal 8// lineage_id: nx_games_page_v2 9// 10// v2 (2026-08-10) closes three MEASURED defects. Full evidence ledger and method: 11// knowledge/papers/games_portal_measured_2026-08-10.md (compiled by nx_paper_forge, 59 cited rows) 12// 13// 1. REFLOW FAILURE. v1's boards were fixed pixel: #c4 was repeat(7,46px) with 5px gaps and 8px 14// padding, an intrinsic minimum of 7*46+6*5+2*8 = 368px against a 320px bar, so it overflowed 15// every phone. v1's @media block resized only #ttt, so republishing v1 would NOT have fixed it. 16// MEASURED on v1's own artifact by nx_uiq_responsive: worst_grid_min_px=368 reflow_fails=1 RED. 17// v2 uses intrinsic tracks (repeat(N,1fr) + aspect-ratio), so a board's minimum width follows 18// its container and cannot be re-fixed by a hardcoded pixel sum. 19// BAR: W3C WCAG 2.1 SC 1.4.10 Reflow (AA) -- an external standard, not a local taste. 20// 2. TWO PORTALS, ONE NAME. /games and /world/games both shipped the same title and h1 with no 21// canonical claim. proxy_routes.conf states the estate's own law -- "subdomain = capability, 22// root path = visitor content" -- and games are visitor content, so /games is canonical. v2 is 23// the single index and lists the /world/* engines as what they are: engine demos. 24// 3. HONEST STATES. v1 advertised chess, go and reversi as "coming soon" with no engine behind any, 25// while 21 real nx_wasm_* game organs were listed nowhere. v2 says Playable here / Engine demo / 26// Not started, and never prints a date it has not earned. 27import "nx_games_js.nx" 28// R-DS 2026-08-25 -- THE PALETTE IS NO LONGER HARDCODED HERE. nx_brand_tokens is the estate's 29// DESIGN-TOKEN SSOT: `token|<group>|<name>|<value>` DATA -> `:root{--nx-<group>-<name>}` via 30// bt_emit_root_buf, and `dark|...` -> `@media(prefers-color-scheme:dark){:root{...}}` via 31// bt_emit_dark_root_buf. BOTH EMITTERS ALREADY SHIPPED; what the estate never had was a 32// nishifamily.com VISITOR-SURFACE emitter that CALLED them. Measured 2026-08-25 by importer 33// census (corpus_complete=1): every one of its 30+ importers is a brand-arc organ, that organ's 34// own gate, or the CLIENT-site builder (nx_web_builder R1b) -- not one nishifamily.com page. 35// ***A TOKEN SSOT THAT NO PAGE EMITTER CALLS IS A SECOND DESIGN SYSTEM, NOT A SHARED ONE***, 36// and on 2026-08-25 the estate had two: this one, and sites/nishifamily/nishi-ds.css, which was 37// referenced by ZERO emitters (0 hits / 23,215 .nx / corpus_complete=1), ZERO stylesheets 38// (0 hits / 81,904 files in sites/ / corpus_complete=1) and not even by the two pages its own 39// header claims it was proven on (funding.html count=0, trade.html count=0). 40import "nx_brand_tokens.nx" 41 42const GM_OUT: *u8 = "web_assets/games.html" 43// This organ also authors the /world/games stub, because that URL was a RIVAL PORTAL: it shipped the 44// same <title> and <h1> as /games, so two pages competed for one query with neither holding a canonical 45// claim, and nothing detected it because neither surface has a proxy_routes.conf entry. One organ owning 46// both surfaces is what stops them drifting apart again. HTML is never hand-written into a docroot. 47const GM_WORLD_OUT: *u8 = "web_assets/world_games.html" 48// The puzzle bank's AUTHORITATIVE form is a checksummed nx_gamesave state file (sudoku_bank.sav). 49// This fragment is DERIVED from it by `nx_sudoku jsbank`, the organ that owns the format, so this 50// emitter parses no data format at all -- it includes bytes produced by the format's owner. 51const GM_BANK: *u8 = "web_assets/sudoku_bank.js" 52const GM_CAP: i64 = 262144 53const GM_BCAP: i64 = 131072 54// Scratch for one emitted token block. Sized from the brand blob itself (21 colour + 2 layout + 55// 2 type tokens, emitted at most twice), not tasted -- and the emit is bounded, so an undersized 56// cap truncates loudly at the FATAL checks below rather than silently shipping half a palette. 57const GM_TOKCAP: i64 = 8192 58// RESPONSIVE BREAKPOINTS. NOT tasted: both are DERIVED from the estate's own design system, 59// sites/nishifamily/nishi-ds.css section 5 RESPONSIVE, which already ships `@media (max-width: 60// 860px)` and `@media (max-width: 640px)`. Reusing its two numbers keeps ONE breakpoint ladder 61// across the estate; inventing a third set here would be the duplicate-ruler defect wearing a 62// constant, and nothing downstream could tell the two ladders apart. 63const GM_BP_MD: i64 = 860 64const GM_BP_SM: i64 = 640 65 66// THE PALETTE, AS DATA (nx_brand_tokens .brand format -- the estate's design-token SSOT). 67// `token|...` = the DEFAULT (light) theme. `dark|...` = the prefers-color-scheme:dark override. 68// A token with NO `dark|` line is THEME-INDEPENDENT by construction, and that is used deliberately: 69// the connect-four board and its discs are a physical OBJECT, not page chrome, so board/board-line/ 70// disc-* carry ONE value in both themes and their contrast is therefore byte-identical to v2's. 71// ***THE FORMAT ALREADY EXPRESSES "THIS DOES NOT FOLLOW THE THEME" AS "NO DARK LINE" -- NO NEW 72// MECHANISM WAS NEEDED, AND INVENTING ONE WOULD HAVE BEEN A SECOND RULER.*** 73// 74// EVERY dark value below is BYTE-IDENTICAL to the literal it replaces in v2, so the dark rendering 75// is unchanged by construction, not by inspection. The light set is new; all 14 of its foreground/ 76// background pairs were measured with nx_contrast and the local WCAG calculator it validated 77// (3/3 agreement to 0.01): worst 5.34:1, against the incumbent dark theme's worst of 5.38:1. 78// Both clear WCAG 2.1 SC 1.4.3 AA (4.5:1). ***A THEME YOU ADD WITHOUT MEASURING ITS PAIRS IS A 79// CONTRAST REGRESSION YOU HAVE NOT FOUND YET.*** 80// disc-r/disc-y are their OWN tokens rather than aliases of --bad/--warn: a game piece and a 81// semantic error state are unrelated purposes, and one constant serving both can be tuned for 82// neither. 83func gm_brand() -> *u8 { 84 return "token|color|bg|#f6f7f4\ntoken|color|ink|#14181f\ntoken|color|primary|#174e5e\ntoken|color|line|#d8dcd5\ntoken|color|muted|#58626e\ntoken|color|panel|#ffffff\ntoken|color|cell|#fbfcfa\ntoken|color|raised|#eef0ec\ntoken|color|fixed|#edefea\ntoken|color|sel|#cfe3ee\ntoken|color|good|#2f6b4e\ntoken|color|good-bg|#e4efe7\ntoken|color|bad|#96372b\ntoken|color|warn|#8f5d18\ntoken|color|soft-bg|#eef0ec\ntoken|color|accent-bg|#e3eef2\ntoken|color|board|#0e2a4a\ntoken|color|board-line|#16385e\ntoken|color|disc-empty|#0b0f14\ntoken|color|disc-r|#f85149\ntoken|color|disc-y|#f5c518\ntoken|layout|wrap|980px\ntoken|layout|gutter|clamp(14px,4vw,20px)\ntoken|font|size-sm|.85rem\ntoken|font|size-base|1rem\ntoken|font|size-lg|1.16rem\ndark|color|bg|#0b0f14\ndark|color|ink|#e6edf3\ndark|color|primary|#4cc2ff\ndark|color|line|#243140\ndark|color|muted|#8aa0b4\ndark|color|panel|#131b24\ndark|color|cell|#0e1722\ndark|color|raised|#1b2733\ndark|color|fixed|#0b1219\ndark|color|sel|#1b3346\ndark|color|good|#3fb950\ndark|color|good-bg|#10261a\ndark|color|bad|#f85149\ndark|color|warn|#f5c518\ndark|color|soft-bg|#1b2230\ndark|color|accent-bg|#0e2233\n" as *u8 85} 86 87func main() -> i64 { 88 let now: i64 = sys_now_realtime_sec() 89 let h: *u8 = sys_mmap(GM_CAP) 90 var w: i64 = 0 91 92 // No puzzle bank is read any more. The sudoku module generates its own proven-unique puzzles 93 // in-image, so there is no data file on this page's critical path at all -- and nothing to keep 94 // in sync. nx_sudoku's bank/jsbank verbs remain useful for offline puzzle sets, but the portal 95 // no longer depends on them. 96 97 // ---------- head ---------- 98 w = gm_cat(h, w, "<!doctype html><html lang='en'><head><meta charset='utf-8'>\n" as *u8) 99 w = gm_cat(h, w, "<meta name='viewport' content='width=device-width,initial-scale=1'>\n" as *u8) 100 w = gm_cat(h, w, "<title>Nishi Games &mdash; free browser games, no accounts, no trackers</title>\n" as *u8) 101 w = gm_cat(h, w, "<meta name='description' content='Free browser games built from scratch: sudoku, connect four, tic-tac-toe and more. No accounts, no trackers, no third-party code. Playable on any phone.'>\n" as *u8) 102 w = gm_cat(h, w, "<link rel='canonical' href='https://nishifamily.com/games'>\n" as *u8) 103 w = gm_cat(h, w, "<style>\n" as *u8) 104 105 // ---------- tokens ---------- 106 // The SSOT emits the token block; this organ no longer writes one. bt_emit_root_buf returns 107 // -1 when NOT ONE token parsed and bt_emit_dark_root_buf returns 0 when the brand carries NO 108 // dark line, so both FATALs below are live non-vacuity assertions on the emitted artifact: 109 // ***AN EMITTER THAT CANNOT FAIL WHEN ITS DATA IS EMPTY WILL SHIP A PAGE WITH NO PALETTE AND 110 // STILL REPORT SUCCESS*** -- absent CSS has no failure mode, exactly like absent code. 111 let bd: *u8 = gm_brand() 112 let bn: i64 = bt_len(bd) 113 let tb: *u8 = sys_mmap(GM_TOKCAP) 114 let lw: i64 = bt_emit_root_buf(bd, bn, tb, GM_TOKCAP) 115 if lw < 0 { sys_write(2, "FATAL: brand parsed ZERO light tokens -- refusing to emit a page with no palette\n" as *u8, 80); sys_exit(2); return 2 } 116 var bi: i64 = 0 117 while bi < lw { h[w] = tb[bi]; w = w + 1; bi = bi + 1 } 118 // Page-local ALIASES over the SSOT names: one indirection, so a brand DATA edit re-themes the 119 // whole page and every rule below keeps reading the short names it always read. 120 w = gm_cat(h, w, ":root{--bg:var(--nx-color-bg);--panel:var(--nx-color-panel);--ink:var(--nx-color-ink);--mut:var(--nx-color-muted);--acc:var(--nx-color-primary);--good:var(--nx-color-good);--bad:var(--nx-color-bad);--warn:var(--nx-color-warn);--line:var(--nx-color-line);--cell:var(--nx-color-cell)}\n" as *u8) 121 let dw: i64 = bt_emit_dark_root_buf(bd, bn, tb, GM_TOKCAP) 122 if dw <= 0 { sys_write(2, "FATAL: brand carries NO dark override -- the page would ship theme-blind\n" as *u8, 72); sys_exit(2); return 2 } 123 var di: i64 = 0 124 while di < dw { h[w] = tb[di]; w = w + 1; di = di + 1 } 125 // Breakpoints redefine TOKENS ONLY, never components -- the same discipline nishi-ds.css states 126 // in its own dark block. A token carries no paint, so a breakpoint cannot restyle anything the 127 // page did not already opt into by reading that token. 128 w = gm_cat(h, w, "@media (max-width:" as *u8) 129 w = gm_catn(h, w, GM_BP_MD) 130 w = gm_cat(h, w, "px){:root{--nx-layout-gutter:clamp(12px,3.6vw,16px)}}\n" as *u8) 131 w = gm_cat(h, w, "@media (max-width:" as *u8) 132 w = gm_catn(h, w, GM_BP_SM) 133 w = gm_cat(h, w, "px){:root{--nx-layout-wrap:100%;--nx-layout-gutter:12px}}\n" as *u8) 134 w = gm_cat(h, w, "*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 system-ui,Segoe UI,Roboto,sans-serif;-webkit-text-size-adjust:100%}\n" as *u8) 135 w = gm_cat(h, w, ":focus-visible{outline:2px solid var(--acc);outline-offset:2px}\n" as *u8) 136 w = gm_cat(h, w, "@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}\n" as *u8) 137 138 // ---------- layout: fluid by construction ---------- 139 w = gm_cat(h, w, "header,main,footer{max-width:var(--nx-layout-wrap);margin:0 auto;padding:0 var(--nx-layout-gutter)}\n" as *u8) 140 w = gm_cat(h, w, "header{padding-top:clamp(24px,6vw,32px)}h1{margin:0;font-size:clamp(1.6rem,6vw,1.95rem);letter-spacing:-.02em;text-wrap:balance}\n" as *u8) 141 w = gm_cat(h, w, ".sub{color:var(--mut);margin:.5em 0 0;max-width:66ch}.stamp{color:var(--mut);font-size:12.5px;margin:.6em 0 0}\n" as *u8) 142 w = gm_cat(h, w, ".note{background:var(--cell);border:1px solid var(--line);border-left:3px solid var(--acc);border-radius:8px;padding:10px 14px;margin:14px 0;color:var(--mut);font-size:13.5px;max-width:78ch}\n" as *u8) 143 w = gm_cat(h, w, "main{padding-bottom:56px}section{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:clamp(14px,4vw,20px);margin:18px 0}\n" as *u8) 144 w = gm_cat(h, w, "h2{margin:.1em 0 .35em;font-size:clamp(1.15rem,4.4vw,1.3rem);letter-spacing:-.015em}code{color:var(--acc);font-size:.92em}\n" as *u8) 145 w = gm_cat(h, w, ".muted{color:var(--mut);font-weight:400;font-size:.9rem}\n" as *u8) 146 w = gm_cat(h, w, "button{font:inherit;cursor:pointer;color:var(--ink)}\n" as *u8) 147 w = gm_cat(h, w, ".btn{background:var(--nx-color-raised);border:1px solid var(--line);border-radius:8px;padding:9px 15px;min-height:40px;touch-action:manipulation}.btn:hover{border-color:var(--acc)}\n" as *u8) 148 w = gm_cat(h, w, ".row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0 0}\n" as *u8) 149 w = gm_cat(h, w, ".status{margin:10px 0 0;color:var(--mut);min-height:1.5em}\n" as *u8) 150 151 // ---------- boards: INTRINSIC tracks. no board carries a hardcoded pixel width. ---------- 152 w = gm_cat(h, w, ".board{display:grid;width:100%;touch-action:manipulation}\n" as *u8) 153 w = gm_cat(h, w, "#ttt{grid-template-columns:repeat(3,1fr);gap:2.2%;max-width:min(100%,330px);margin:12px 0}\n" as *u8) 154 w = gm_cat(h, w, "#ttt .cell{aspect-ratio:1;width:100%;font-size:clamp(1.5rem,9vw,2.1rem);font-weight:700;background:var(--cell);border:1px solid var(--line);border-radius:8px;min-height:0}\n" as *u8) 155 w = gm_cat(h, w, "#ttt .cell:hover{border-color:var(--acc)}\n" as *u8) 156 w = gm_cat(h, w, "#c4{grid-template-columns:repeat(7,1fr);gap:1.6%;background:var(--nx-color-board);padding:1.6%;border-radius:10px;max-width:min(100%,420px);margin:12px 0}\n" as *u8) 157 w = gm_cat(h, w, "#c4 .disc{aspect-ratio:1;width:100%;border-radius:50%;background:var(--nx-color-disc-empty);border:1px solid var(--nx-color-board-line);padding:0}\n" as *u8) 158 w = gm_cat(h, w, "#c4 .disc:hover{border-color:var(--acc)}#c4 .r{background:var(--nx-color-disc-r);border-color:var(--nx-color-disc-r)}#c4 .y{background:var(--nx-color-disc-y);border-color:var(--nx-color-disc-y)}\n" as *u8) 159 w = gm_cat(h, w, "#sud{grid-template-columns:repeat(9,1fr);gap:0;max-width:min(100%,460px);margin:12px 0;border:2px solid var(--mut);border-radius:6px;overflow:hidden}\n" as *u8) 160 w = gm_cat(h, w, "#sud .sc{aspect-ratio:1;width:100%;border:0;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:var(--cell);font-size:clamp(.85rem,3.6vw,1.15rem);font-weight:600;padding:0;display:flex;align-items:center;justify-content:center}\n" as *u8) 161 w = gm_cat(h, w, "#sud .sc.b3{border-right-width:2px;border-right-color:var(--mut)}#sud .sc.b9{border-bottom-width:2px;border-bottom-color:var(--mut)}\n" as *u8) 162 w = gm_cat(h, w, "#sud .sc.fx{color:var(--mut);background:var(--nx-color-fixed)}#sud .sc.sel{background:var(--nx-color-sel);outline:2px solid var(--acc);outline-offset:-2px}\n" as *u8) 163 w = gm_cat(h, w, "#sud .sc.bad{color:var(--bad)}\n" as *u8) 164 w = gm_cat(h, w, "#pad{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;max-width:min(100%,460px);margin:10px 0 0}\n" as *u8) 165 w = gm_cat(h, w, "#pad button{aspect-ratio:1.35;background:var(--nx-color-raised);border:1px solid var(--line);border-radius:8px;font-size:clamp(1rem,4.4vw,1.2rem);font-weight:600;touch-action:manipulation;padding:0}\n" as *u8) 166 w = gm_cat(h, w, "#pad button:hover{border-color:var(--acc)}\n" as *u8) 167 168 // ---------- card grid ---------- 169 w = gm_cat(h, w, ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,170px),1fr));gap:12px;margin:12px 0 0}\n" as *u8) 170 w = gm_cat(h, w, ".card{background:var(--cell);border:1px solid var(--line);border-radius:10px;padding:14px;text-decoration:none;color:inherit;display:block}\n" as *u8) 171 w = gm_cat(h, w, "a.card:hover{border-color:var(--acc)}.card h3{margin:0 0 4px;font-size:.98rem}.card .s{font-size:12.5px;color:var(--mut);line-height:1.45}\n" as *u8) 172 w = gm_cat(h, w, ".tag{display:inline-block;font-size:11px;padding:3px 8px;border-radius:20px;margin-top:10px}\n" as *u8) 173 w = gm_cat(h, w, ".live{background:var(--nx-color-good-bg);color:var(--good)}.soon{background:var(--nx-color-soft-bg);color:var(--mut)}.eng{background:var(--nx-color-accent-bg);color:var(--acc)}\n" as *u8) 174 w = gm_cat(h, w, "footer{color:var(--mut);font-size:12.5px;padding-bottom:40px}a{color:var(--acc)}\n" as *u8) 175 w = gm_cat(h, w, "</style></head><body>\n" as *u8) 176 177 // ---------- header ---------- 178 w = gm_cat(h, w, "<header><h1>Nishi Games</h1>\n" as *u8) 179 w = gm_cat(h, w, "<p class='sub'>Free browser games, built from scratch on the Nishi stack and playable right here &mdash; on a phone as well as a desktop. No accounts, no trackers, no third-party code.</p>\n" as *u8) 180 w = gm_cat(h, w, "<div class='note'><b>How these are built:</b> every game here is an original implementation. The <i>rules</i> of sudoku, chess, go, checkers and connect four are not copyrightable, so we build them clean &mdash; no ROMs are rehosted and no third-party code runs. Freely-licensed ROMs may be added later, but only behind a verified-license provenance gate: &ldquo;available on archive.org&rdquo; is not the same as &ldquo;legal to rehost&rdquo;.</div>\n" as *u8) 181 w = gm_cat(h, w, "<p class='stamp'>Emitted by <code>nx_games_page</code> at epoch " as *u8) 182 w = gm_catn(h, w, now) 183 w = gm_cat(h, w, " &middot; layout held to WCAG&nbsp;2.1 SC&nbsp;1.4.10 Reflow by <code>nx_uiq_responsive</code>.</p></header>\n" as *u8) 184 w = gm_cat(h, w, "<main>\n" as *u8) 185 186 // ---------- SUDOKU: its own page, because it is a SOVEREIGN wasm module ---------- 187 // Sudoku no longer runs here. It is one NishiLang module compiled to WebAssembly by the Nishi 188 // toolchain and served at /games/sudoku, where the page holds no game logic at all -- it blits a 189 // framebuffer and forwards input. Keeping a second JS implementation on this page would have been 190 // the duplicate-ruler defect wearing a different hat. 191 w = gm_cat(h, w, "<section><h2>Sudoku <span class='muted'>&middot; sovereign WebAssembly</span></h2>\n" as *u8) 192 w = gm_cat(h, w, "<p class='muted'>Every puzzle is proven to have exactly one solution &mdash; the generator counts solutions rather than assuming, and never goes below 17 clues, the proven minimum. The board, the rules, the generator and the renderer are a single NishiLang module compiled to WebAssembly; the page it runs on holds no game logic.</p>\n" as *u8) 193 w = gm_cat(h, w, "<div class='row'><a class='btn' href='/games/sudoku'>Play Sudoku &rarr;</a></div></section>\n" as *u8) 194 195 // ---------- CONNECT FOUR ---------- 196 w = gm_cat(h, w, "<section><h2>Connect Four <span class='muted'>&middot; vs alpha-beta AI</span></h2>\n" as *u8) 197 w = gm_cat(h, w, "<p class='muted'>You are red. Tap any square in a column to drop a disc; get four in a row &mdash; across, down or diagonally. The AI searches 5 plies with alpha-beta pruning.</p>\n" as *u8) 198 w = gm_cat(h, w, "<div class='board' id='c4'></div>\n" as *u8) 199 w = gm_cat(h, w, "<div class='status' id='c4-status'>Your move &mdash; you are red.</div>\n" as *u8) 200 w = gm_cat(h, w, "<div class='row'><button class='btn' onclick='creset()'>New game</button></div></section>\n" as *u8) 201 202 // ---------- TIC-TAC-TOE ---------- 203 w = gm_cat(h, w, "<section><h2>Tic-Tac-Toe <span class='muted'>&middot; vs unbeatable AI</span></h2>\n" as *u8) 204 w = gm_cat(h, w, "<p class='muted'>You are X and move first. The AI plays a full minimax search, so it never loses &mdash; the best you can force is a draw.</p>\n" as *u8) 205 w = gm_cat(h, w, "<div class='board' id='ttt'></div>\n" as *u8) 206 w = gm_cat(h, w, "<div class='status' id='ttt-status'>Your move (X).</div>\n" as *u8) 207 w = gm_cat(h, w, "<div class='row'><button class='btn' onclick='treset()'>New game</button></div></section>\n" as *u8) 208 209 // ---------- LIVE MATCHES: sovereign multiplayer over the family message store (C12) ---------- 210 // DATA-DRIVEN: lists whatever match pages exist in the docroot live/ dir at emit time 211 // (nx_game_chat emit writes them). No hand-kept list to go stale -- re-emit and a new match 212 // appears; an empty dir says so honestly instead of hiding the section. 213 w = gm_cat(h, w, "<section><h2>Play together <span class='muted'>&middot; sovereign multiplayer, no middleman</span></h2>\n" as *u8) 214 w = gm_cat(h, w, "<p class='muted'>Matches below were played <b>through the Nishi message store</b>: every move is a message in a room on the family NAS, the board is a deterministic replay of that log, and table chat rides the same room. No game server, no third-party relay &mdash; a match a family plays on their LAN never leaves it. The same store carries the video-room chat; the interactive in-page client is the next rung on <a href='/compare/comms'>the comms roadmap</a>.</p>\n" as *u8) 215 w = gm_cat(h, w, "<div class='grid'>\n" as *u8) 216 var lm: i64 = 0 217 let dfd: i64 = sys_openat_rd("sites/nishifamily/games/live" as *u8) 218 if dfd >= 0 { 219 let db: *u8 = sys_mmap(65536) 220 var more: i64 = 1 221 while more == 1 { 222 let dn: i64 = sys_getdents64(dfd, db, 65536) 223 if dn <= 0 { more = 0 } else { 224 var off: i64 = 0 225 while off < dn { 226 let rec: *u8 = (db as i64 + off) as *u8 227 let rl: i64 = dirent_reclen(rec) 228 if dirent_type(rec) == DT_REG { 229 let nm: *u8 = dirent_name(rec) 230 var nl: i64 = 0 231 while nm[nl] != (0 as u8) { nl = nl + 1 } 232 if nl > 5 { 233 if nm[nl-5] == (46 as u8) { if nm[nl-4] == (104 as u8) { if nm[nl-3] == (116 as u8) { if nm[nl-2] == (109 as u8) { if nm[nl-1] == (108 as u8) { 234 w = gm_cat(h, w, "<a class='card' href='/games/live/" as *u8) 235 w = gm_cat(h, w, nm) 236 w = gm_cat(h, w, "'><h3>" as *u8) 237 w = gm_cat(h, w, nm) 238 w = gm_cat(h, w, "</h3><div class='s'>Played over the sovereign store &mdash; open for the final board, the replay counts and the table chat.</div><span class='tag live'>Live match</span></a>\n" as *u8) 239 lm = lm + 1 240 } } } } } 241 } 242 } 243 off = off + rl 244 } 245 } 246 } 247 sys_close(dfd) 248 } 249 if lm == 0 { 250 w = gm_cat(h, w, "<div class='card'><h3>No matches published yet</h3><div class='s'>The first match emitted by nx_game_chat will appear here on the next page emit.</div></div>\n" as *u8) 251 } 252 w = gm_cat(h, w, "</div></section>\n" as *u8) 253 254 // ---------- inventory: honest states, /world/* demos folded in so there is ONE portal ---------- 255 w = gm_cat(h, w, "<section><h2>Every game in the estate</h2>\n" as *u8) 256 w = gm_cat(h, w, "<p class='muted'>This is the whole list, and it is honest about status. <b>Playable here</b> runs on this page. <b>Engine demo</b> is a sovereign engine running on its own page. <b>Not started</b> means there is no engine yet &mdash; we do not print a date we have not earned.</p>\n" as *u8) 257 w = gm_cat(h, w, "<div class='grid'>\n" as *u8) 258 w = gm_card(h, w, "Sudoku" as *u8, "Unique-solution generator, 17-clue floor" as *u8, "" as *u8, 0) 259 w = gm_card(h, w, "Connect Four" as *u8, "Alpha-beta, 5-ply" as *u8, "" as *u8, 0) 260 w = gm_card(h, w, "Tic-Tac-Toe" as *u8, "Unbeatable minimax" as *u8, "" as *u8, 0) 261 w = gm_card(h, w, "Nishi Craft" as *u8, "First-person voxel world, NishiLang to WebAssembly" as *u8, "/world/craft" as *u8, 1) 262 w = gm_card(h, w, "Nishi Frontier" as *u8, "Trade and strategy across a star map" as *u8, "/world/frontier" as *u8, 1) 263 w = gm_card(h, w, "Nishi Drift" as *u8, "Survival on a failing salvage hauler" as *u8, "/world/drift" as *u8, 1) 264 w = gm_card(h, w, "Nishi Glade" as *u8, "Action-adventure overworld and dungeon" as *u8, "/world/explore" as *u8, 1) 265 w = gm_card(h, w, "Nishi Shore" as *u8, "Shore stage on the Craft engine" as *u8, "/world/beach" as *u8, 1) 266 w = gm_card(h, w, "Collide Lab" as *u8, "Swept collision vs the tunnelling bug" as *u8, "/world/collide" as *u8, 1) 267 w = gm_card(h, w, "VN Scene" as *u8, "Sprite compositor with dissolve transitions" as *u8, "/world/vn" as *u8, 1) 268 w = gm_card(h, w, "Checkers" as *u8, "Sovereign engine exists; browser wiring in progress" as *u8, "" as *u8, 2) 269 w = gm_card(h, w, "Crossword" as *u8, "Needs a dictionary corpus and a fill solver" as *u8, "" as *u8, 2) 270 w = gm_card(h, w, "Chess" as *u8, "Move generator will be held to published perft counts" as *u8, "" as *u8, 2) 271 w = gm_card(h, w, "Reversi / Othello" as *u8, "No engine yet" as *u8, "" as *u8, 2) 272 w = gm_card(h, w, "Go" as *u8, "No engine yet" as *u8, "" as *u8, 2) 273 w = gm_cat(h, w, "</div></section>\n" as *u8) 274 275 w = gm_cat(h, w, "</main>\n" as *u8) 276 w = gm_cat(h, w, "<footer>Sovereign Nishi stack &middot; emitted by <code>nx_games_page</code> &middot; original implementations, no ROMs &middot; nishifamily.com/games</footer>\n" as *u8) 277 278 // ---------- scripts (bank as data, then behaviour) ---------- 279 w = gm_cat(h, w, "<script>\n" as *u8) 280 w = gjs_scripts(h, w) 281 w = gm_cat(h, w, "</script>\n" as *u8) 282 w = gm_cat(h, w, "</body></html>\n" as *u8) 283 284 // ---------- write ---------- 285 let fd: i64 = sys_openat_wr(GM_OUT, 0x1a4) 286 if fd < 0 { sys_write(2, "FATAL: cannot open web_assets/games.html\n" as *u8, 41); sys_exit(2); return 2 } 287 sys_write(fd, h, w) 288 sys_close(fd) 289 sys_write(1, "[nx_games_page] wrote games.html bytes=" as *u8, 39) 290 let nb: *u8 = sys_mmap(28) 291 var k2: i64 = 0 292 var m2: i64 = w 293 if m2 == 0 { nb[0] = 48 as u8; k2 = 1 } 294 while m2 > 0 { nb[k2] = (48 + (m2 % 10)) as u8; m2 = m2 / 10; k2 = k2 + 1 } 295 var qd: i64 = k2 - 1 296 while qd >= 0 { let o1: *u8 = sys_mmap(1); o1[0] = nb[qd]; sys_write(1, o1, 1); qd = qd - 1 } 297 sys_write(1, "\n" as *u8, 1) 298 299 // ---------- the /world/games stub ---------- 300 // It keeps the URL alive (nothing 404s for anyone who bookmarked it) while ending the title 301 // collision: canonical points at /games, the heading says what this page now is, and the redirect 302 // is declared three ways so it works with JS off and with meta-refresh ignored. 303 var v: i64 = 0 304 let g2: *u8 = sys_mmap(8192) 305 v = gm_cat(g2, v, "<!doctype html><html lang='en'><head><meta charset='utf-8'>\n" as *u8) 306 v = gm_cat(g2, v, "<meta name='viewport' content='width=device-width,initial-scale=1'>\n" as *u8) 307 v = gm_cat(g2, v, "<title>Nishi engine demos &mdash; moved to /games</title>\n" as *u8) 308 v = gm_cat(g2, v, "<link rel='canonical' href='https://nishifamily.com/games'>\n" as *u8) 309 v = gm_cat(g2, v, "<meta name='robots' content='noindex,follow'>\n" as *u8) 310 v = gm_cat(g2, v, "<meta http-equiv='refresh' content='0;url=/games'>\n" as *u8) 311 // The stub gets the SAME SSOT palette as the portal -- one brand, two surfaces. Emitting it 312 // twice from one gm_brand() is what makes a drift between them impossible; two hand-written 313 // :root blocks is exactly how /games and /world/games drifted into rival portals in v1. 314 v = gm_cat(g2, v, "<style>" as *u8) 315 let sl: i64 = bt_emit_root_buf(bd, bn, tb, GM_TOKCAP) 316 if sl < 0 { sys_write(2, "FATAL: stub brand parsed ZERO light tokens\n" as *u8, 42); sys_exit(2); return 2 } 317 var si: i64 = 0 318 while si < sl { g2[v] = tb[si]; v = v + 1; si = si + 1 } 319 v = gm_cat(g2, v, ":root{--bg:var(--nx-color-bg);--ink:var(--nx-color-ink);--mut:var(--nx-color-muted);--acc:var(--nx-color-primary)}\n" as *u8) 320 let sd: i64 = bt_emit_dark_root_buf(bd, bn, tb, GM_TOKCAP) 321 if sd <= 0 { sys_write(2, "FATAL: stub brand carries NO dark override\n" as *u8, 42); sys_exit(2); return 2 } 322 var sj: i64 = 0 323 while sj < sd { g2[v] = tb[sj]; v = v + 1; sj = sj + 1 } 324 v = gm_cat(g2, v, "@media (max-width:" as *u8) 325 v = gm_catn(g2, v, GM_BP_SM) 326 v = gm_cat(g2, v, "px){:root{--nx-layout-gutter:12px}}\n" as *u8) 327 v = gm_cat(g2, v, "*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 system-ui,Segoe UI,Roboto,sans-serif}\n" as *u8) 328 v = gm_cat(g2, v, "main{max-width:640px;margin:0 auto;padding:clamp(28px,8vw,64px) var(--nx-layout-gutter)}\n" as *u8) 329 v = gm_cat(g2, v, "h1{font-size:clamp(1.4rem,5.5vw,1.8rem);margin:0 0 .4em;letter-spacing:-.02em}\n" as *u8) 330 v = gm_cat(g2, v, "p{color:var(--mut);max-width:60ch;font-size:var(--nx-font-size-base)}a{color:var(--acc)}.fine{font-size:var(--nx-font-size-sm)}\n" as *u8) 331 v = gm_cat(g2, v, ".btn{display:inline-block;margin-top:14px;background:var(--nx-color-raised);border:1px solid var(--nx-color-line);border-radius:8px;padding:11px 18px;min-height:44px;font-size:var(--nx-font-size-base);color:var(--ink);text-decoration:none}\n" as *u8) 332 v = gm_cat(g2, v, ".btn:hover{border-color:var(--acc)}:focus-visible{outline:2px solid var(--acc);outline-offset:2px}\n" as *u8) 333 v = gm_cat(g2, v, "@media(prefers-reduced-motion:reduce){*{transition:none!important}}</style></head><body>\n" as *u8) 334 v = gm_cat(g2, v, "<main><h1>These demos now live at /games</h1>\n" as *u8) 335 v = gm_cat(g2, v, "<p>This page used to be a second games portal with the same name as the main one, which meant two pages competed to be the games home and neither was authoritative. The sovereign engine demos &mdash; Craft, Frontier, Drift, Glade, Shore, Collide Lab and the VN scene &mdash; are all listed on the one portal now, alongside the games you can play in the page.</p>\n" as *u8) 336 v = gm_cat(g2, v, "<p><a class='btn' href='/games'>Go to Nishi Games &rarr;</a></p>\n" as *u8) 337 v = gm_cat(g2, v, "<p class='fine'>If you are not redirected automatically, use the link above.</p></main>\n" as *u8) 338 v = gm_cat(g2, v, "<script>location.replace('/games');</script>\n" as *u8) 339 v = gm_cat(g2, v, "</body></html>\n" as *u8) 340 let wfd: i64 = sys_openat_wr(GM_WORLD_OUT, 0x1a4) 341 if wfd < 0 { sys_write(2, "FATAL: cannot open web_assets/world_games.html\n" as *u8, 47); sys_exit(2); return 2 } 342 sys_write(wfd, g2, v) 343 sys_close(wfd) 344 sys_write(1, "[nx_games_page] wrote world_games.html (canonical stub) bytes=" as *u8, 62) 345 let wb: *u8 = sys_mmap(28) 346 var wk: i64 = 0 347 var wm: i64 = v 348 if wm == 0 { wb[0] = 48 as u8; wk = 1 } 349 while wm > 0 { wb[wk] = (48 + (wm % 10)) as u8; wm = wm / 10; wk = wk + 1 } 350 var wq: i64 = wk - 1 351 while wq >= 0 { let o3: *u8 = sys_mmap(1); o3[0] = wb[wq]; sys_write(1, o3, 1); wq = wq - 1 } 352 sys_write(1, "\n" as *u8, 1) 353 354 sys_exit(0) 355 return 0 356}