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 &mdash; 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 &mdash; 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&hellip;</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 &mdash; 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}