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 — 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 — 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 — 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: “available on archive.org” is not the same as “legal to rehost”.</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, " · layout held to WCAG 2.1 SC 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'>· 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 — 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 →</a></div></section>\n" as *u8)
194
195 // ---------- CONNECT FOUR ----------
196 w = gm_cat(h, w, "<section><h2>Connect Four <span class='muted'>· 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 — 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 — 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'>· 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 — 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'>· 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 — 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 — 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 — 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 · emitted by <code>nx_games_page</code> · original implementations, no ROMs · 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 — 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 — Craft, Frontier, Drift, Glade, Shore, Collide Lab and the VN scene — 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 →</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}