code wiki / _hdl_build / nx_sudoku_html.nx
nx_sudoku_html.nx source
↩ module page · 133 lines · 10334 B
1// nx_sudoku_html.nx -- SOVEREIGN packager: the sudoku wasm module -> a self-contained playable page.
2//
3// WHY NOT nx_wasm_html. That packager's shim is hardcoded for an f32 demo: it calls
4// render(yc,ys,xc,xs) with rotation matrices, never calls init() or tick(), and pins the canvas at a
5// fixed 512px with height:100vh -- so it cannot drive a turn-based game and cannot pass the estate's
6// reflow bar on a phone. A packager is part of the contract, not a detail: a game whose interface is
7// init/tick/render needs a shim that speaks it.
8//
9// THE SOVEREIGN BOUNDARY, stated plainly. The emitted JS is a CANVAS SHIM and nothing else -- the
10// accepted foreign-browser last mile, the same position nx_wasm_html takes. It knows how to blit a
11// framebuffer and how to turn a keypress or a touch into an integer. It contains NO rules, NO solver,
12// NO generator, NO scoring and NO grid geometry: cell hit testing is an export (tap), so the page
13// cannot drift out of sync with the board it is drawing. Every decision is NishiLang.
14//
15// RESPONSIVE BY CONSTRUCTION (WCAG 2.1 SC 1.4.10 Reflow @ 320px): the canvas carries no fixed pixel
16// width. It is width:100% inside a max-width container with aspect-ratio:1, and image-rendering
17// pixelated keeps the integer raster crisp when it scales. Taps are mapped back through the canvas
18// rect, so the module always receives framebuffer coordinates whatever size it is displayed at.
19//
20// usage: nx_sudoku_html <in.wasm> <out.html>
21// exit : 0 ok | 2 usage | 3 cannot read wasm
22// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
23import "nx_syscalls.nx"
24import "nx_base64.nx"
25import "nx_wasmfit.nx"
26const K_MAGIC_8192: i64 = 8192
27
28func hw(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 }
29
30func main(argc: i64, argv: *i64) -> i64 {
31 if argc < 3 { hw(2, "usage: nx_sudoku_html <in.wasm> <out.html>\n" as *u8); sys_exit(2); return 2 }
32 let wpath: *u8 = argv[1] as *u8
33 let hpath: *u8 = argv[2] as *u8
34
35 let wlen_p: *i64 = sys_mmap(8) as *i64
36 let wbytes: *u8 = sys_read_file(wpath, wlen_p)
37 if (wbytes as i64) == 0 { hw(2, "nx_sudoku_html: read wasm failed\n" as *u8); sys_exit(3); return 3 }
38 let wlen: i64 = wlen_p[0]
39 // The estate's ONE fit ruler (nx_wasmfit). This emitter does not go through gpe_build_s, where the
40 // guard used to live, so it is wired here explicitly; nx_wasmemit_guard_gate enforces that it stays.
41 if wf_guard(wbytes, wlen, "nx_sudoku_html" as *u8) == WF_REFUSE { return 5 }
42
43 // sovereign base64 codec -- no node, no external tool in the path
44 // 4 output chars per 3 input bytes, plus padding and slack.
45 let b64cap: i64 = (wlen/3)*4 + K_MAGIC_8192
46 let b64: *u8 = sys_mmap(b64cap)
47 let b64len: i64 = b64_encode(wbytes, wlen, b64)
48 if b64len <= 0 { hw(2, "nx_sudoku_html: base64 encode failed\n" as *u8); sys_exit(3); return 3 }
49
50 let fd: i64 = sys_openat_wr(hpath, 0x1a4)
51 if fd < 0 { hw(2, "nx_sudoku_html: cannot open output\n" as *u8); sys_exit(3); return 3 }
52
53 hw(fd, "<!doctype html><html lang='en'><head><meta charset='utf-8'>\n" as *u8)
54 hw(fd, "<meta name='viewport' content='width=device-width,initial-scale=1'>\n" as *u8)
55 hw(fd, "<title>Sudoku — Nishi Games</title>\n" as *u8)
56 hw(fd, "<meta name='description' content='Free sudoku in your browser. Every puzzle proven to have exactly one solution. No accounts, no trackers, no third-party code.'>\n" as *u8)
57 hw(fd, "<link rel='canonical' href='https://nishifamily.com/games/sudoku'>\n" as *u8)
58 hw(fd, "<style>\n" as *u8)
59 hw(fd, ":root{--bg:#0b0f14;--panel:#131b24;--ink:#e6edf3;--mut:#8aa0b4;--acc:#4cc2ff;--line:#243140}\n" as *u8)
60 hw(fd, "*{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)
61 hw(fd, ":focus-visible{outline:2px solid var(--acc);outline-offset:2px}\n" as *u8)
62 hw(fd, "@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}\n" as *u8)
63 hw(fd, "main{max-width:520px;margin:0 auto;padding:clamp(14px,4vw,24px) clamp(12px,4vw,20px) 48px}\n" as *u8)
64 hw(fd, "h1{margin:0 0 .2em;font-size:clamp(1.5rem,6vw,1.9rem);letter-spacing:-.02em}\n" as *u8)
65 hw(fd, "p.sub{color:var(--mut);margin:.2em 0 1em;font-size:.95rem}\n" as *u8)
66 // the board: NO fixed pixel width. width:100% + aspect-ratio is what makes 320px conform.
67 hw(fd, "#wrap{width:100%;max-width:460px;margin:0 auto}\n" as *u8)
68 hw(fd, "canvas{width:100%;height:auto;aspect-ratio:1;display:block;image-rendering:pixelated;background:var(--panel);border:1px solid var(--line);border-radius:10px;touch-action:manipulation}\n" as *u8)
69 hw(fd, "#pad{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin:12px auto 0;width:100%;max-width:460px}\n" as *u8)
70 hw(fd, "#pad button{aspect-ratio:1.35;background:#1b2733;color:var(--ink);border:1px solid var(--line);border-radius:8px;font:600 clamp(1rem,4.4vw,1.2rem)/1 system-ui,sans-serif;cursor:pointer;touch-action:manipulation;padding:0}\n" as *u8)
71 hw(fd, "#pad button:hover{border-color:var(--acc)}\n" as *u8)
72 hw(fd, "#row{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0 0}\n" as *u8)
73 hw(fd, ".btn{background:#1b2733;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 15px;min-height:40px;font:inherit;cursor:pointer;touch-action:manipulation}\n" as *u8)
74 hw(fd, ".btn:hover{border-color:var(--acc)}\n" as *u8)
75 hw(fd, "#stat{color:var(--mut);margin:12px 0 0;min-height:1.5em}\n" as *u8)
76 hw(fd, "footer{color:var(--mut);font-size:12.5px;margin-top:22px}code{color:var(--acc);font-size:.92em}\n" as *u8)
77 hw(fd, "</style></head><body>\n" as *u8)
78
79 hw(fd, "<main><h1>Sudoku</h1>\n" as *u8)
80 hw(fd, "<p class='sub'>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. Tap a square, then a number.</p>\n" as *u8)
81 hw(fd, "<div id='wrap'><canvas id='c' aria-label='Sudoku board'></canvas></div>\n" as *u8)
82 hw(fd, "<div id='pad'></div>\n" as *u8)
83 hw(fd, "<div id='row'><button class='btn' id='bnew'>New puzzle</button><button class='btn' id='bchk'>Check</button></div>\n" as *u8)
84 hw(fd, "<p id='stat'>Loading…</p>\n" as *u8)
85 hw(fd, "<footer>The board, the rules, the generator and the renderer are one NishiLang module compiled to WebAssembly by the Nishi toolchain. This page only blits a framebuffer and forwards input — it holds no game logic. Emitted by <code>nx_sudoku_html</code>.</footer></main>\n" as *u8)
86
87 hw(fd, "<script>\n" as *u8)
88 hw(fd, "var B='" as *u8)
89 sys_write(fd, b64, b64len)
90 hw(fd, "';\n" as *u8)
91 // ---- the canvas shim. Blit + input mapping ONLY. No rules live here. ----
92 hw(fd, "var by=Uint8Array.from(atob(B),function(c){return c.charCodeAt(0)});\n" as *u8)
93 hw(fd, "WebAssembly.instantiate(by).then(function(r){var ex=r.instance.exports;\n" as *u8)
94 hw(fd, "var W=Number(ex.ww()),H=Number(ex.hh()),off=Number(ex.fb_off());\n" as *u8)
95 hw(fd, "var cv=document.getElementById('c');cv.width=W;cv.height=H;\n" as *u8)
96 hw(fd, "var ctx=cv.getContext('2d');var img=ctx.createImageData(W,H);\n" as *u8)
97 hw(fd, "var mem=new Uint8Array(ex.memory.buffer);\n" as *u8)
98 // blit: the module packs one RGB pixel per i64, so stride is 8 bytes
99 hw(fd, "function blit(){for(var i=0;i<W*H;i++){var b=off+i*8;img.data[i*4]=mem[b];img.data[i*4+1]=mem[b+1];img.data[i*4+2]=mem[b+2];img.data[i*4+3]=255;}ctx.putImageData(img,0,0);}\n" as *u8)
100 hw(fd, "function stat(){var s=Number(ex.solved()),c=Number(ex.clues());\n" as *u8)
101 hw(fd, "document.getElementById('stat').textContent=s==1?'Solved. Every square correct.':(''+c+' clues, exactly one solution. Filled '+Number(ex.score())+' of '+(81-c)+'.');}\n" as *u8)
102 hw(fd, "function step(k){ex.tick(BigInt(k));blit();stat();}\n" as *u8)
103 // taps: map client coords back through the canvas rect into FRAMEBUFFER coords, so the module is
104 // handed board pixels no matter what size the canvas is displayed at. The page never computes a cell.
105 hw(fd, "function at(e){var r=cv.getBoundingClientRect();var t=(e.touches&&e.touches[0])?e.touches[0]:e;\n" as *u8)
106 hw(fd, "var x=Math.floor((t.clientX-r.left)*W/r.width),y=Math.floor((t.clientY-r.top)*H/r.height);\n" as *u8)
107 hw(fd, "ex.tap(BigInt(x),BigInt(y));blit();stat();}\n" as *u8)
108 hw(fd, "cv.addEventListener('click',at);\n" as *u8)
109 hw(fd, "cv.addEventListener('touchstart',function(e){e.preventDefault();at(e);},{passive:false});\n" as *u8)
110 // number pad: essential on a phone, where there is no keyboard
111 hw(fd, "var pad=document.getElementById('pad');\n" as *u8)
112 hw(fd, "for(var v=1;v<=9;v++){(function(n){var b=document.createElement('button');b.textContent=''+n;\n" as *u8)
113 hw(fd, "b.setAttribute('aria-label','place '+n);b.onclick=function(){step(n);};pad.appendChild(b);})(v);}\n" as *u8)
114 hw(fd, "var er=document.createElement('button');er.textContent='Erase';er.setAttribute('aria-label','erase square');er.onclick=function(){step(0);};pad.appendChild(er);\n" as *u8)
115 hw(fd, "document.getElementById('bnew').onclick=function(){step(20);};\n" as *u8)
116 hw(fd, "document.getElementById('bchk').onclick=function(){step(21);};\n" as *u8)
117 hw(fd, "document.addEventListener('keydown',function(e){var k=e.key;\n" as *u8)
118 hw(fd, "if(k>='1'&&k<='9'){step(parseInt(k,10));return;}\n" as *u8)
119 hw(fd, "if(k=='0'||k=='Backspace'||k=='Delete'){step(0);return;}\n" as *u8)
120 hw(fd, "if(k=='ArrowLeft'){step(10);return;}if(k=='ArrowRight'){step(11);return;}\n" as *u8)
121 hw(fd, "if(k=='ArrowUp'){step(12);return;}if(k=='ArrowDown'){step(13);return;}\n" as *u8)
122 hw(fd, "if(k=='n'||k=='N'){step(20);return;}if(k=='c'||k=='C'){step(21);}});\n" as *u8)
123 hw(fd, "ex.init();blit();stat();\n" as *u8)
124 hw(fd, "}).catch(function(e){document.getElementById('stat').textContent='This game needs WebAssembly: '+e.message;});\n" as *u8)
125 hw(fd, "</script></body></html>\n" as *u8)
126
127 sys_close(fd)
128 hw(1, "nx_sudoku_html: wrote " as *u8)
129 hw(1, hpath)
130 hw(1, "\n" as *u8)
131 sys_exit(0)
132 return 0
133}