code wiki / _hdl_build / nx_sudoku_wasm.nx

nx_sudoku_wasm.nx source

↩ module page · 275 lines · 10189 B

1// nx_sudoku_wasm.nx -- SOVEREIGN SUDOKU as a browser game: pure-Nishi, NO-FLOAT, BASE-RELATIVE. 2// The same source is the native binary (base = an mmap'd buffer, provable by a gate) and the 3// WebAssembly module (base = 0). Exports the estate's established game interface -- 4// init / tick / render / score / ww / hh / fb_off -- so the SAME blit-only HTML packager that serves 5// nx_wasm_2048 and nx_wasm_craft serves this, and THE PAGE HOLDS NO GAME LOGIC: it copies a 6// framebuffer and forwards keys. 7// 8// WHY THIS REPLACES THE JAVASCRIPT. The /games sudoku previously ran hand-written JS: a second 9// implementation of a ruleset we already had in NishiLang, untestable by any estate gate, and not 10// sovereign in any sense. Rules, solver, generator, renderer and input now live in one NishiLang 11// module that a gate can drive natively. There is no bank file, no data format, no host cooperation 12// and nothing to keep in sync -- the module GENERATES its own proven-unique puzzles in-image. 13// 14// UNIQUENESS IS COUNTED, NOT ASSUMED, and it is not counted here: the one solver/counter/carver lives 15// in nx_sudoku_core.nx and is shared with nx_sudoku (the bank tool). Composing it rather than copying 16// it is the point -- two implementations of one ruleset is the duplicate-ruler defect. 17// 18// Clue floor 17: McGuire, Tugemann & Civario (2012) proved no 16-clue sudoku has a unique solution. 19// license_tier: ORIGINAL No hw writes (Rule 26). 20import "nx_nishi_font_core.nx" 21import "nx_sudoku_core.nx" 22const O_MAGIC_20260810: i64 = 20260810 23const O_MAGIC_7919: i64 = 7919 24 25const W: i64 = 288 26const H: i64 = 288 27const MARGIN: i64 = 9 28const CELL: i64 = 30 29 30// ---- flat memory image, all base-relative ---- 31const O_FB: i64 = 0 // W*H i64 packed RGB = 663552 32const O_CORE: i64 = 663552 // SC_SIZE (8432): the shared solver's working area 33const O_CUR: i64 = 671984 // 81 i64: the player's board (starts as the givens) 34const O_ST: i64 = 672632 // 8 i64 state 35const O_MSK: i64 = 672696 // 9216 B font glyph scratch 36const O_STR: i64 = 681912 // digit string scratch 37const O_END: i64 = 681944 // total bytes a host must provide 38 39// state slots 40const S_SEL: i64 = 0 // selected cell 0..80, or -1 41const S_SOLVED: i64 = 1 42const S_SHOW: i64 = 2 // reveal wrong digits (only when the player asks) 43const S_CLUES: i64 = 3 44const S_SEED: i64 = 4 45 46func rgb(r: i64, g: i64, b: i64) -> i64 { return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16) } 47 48func fillr(base: i64, x0: i64, y0: i64, x1: i64, y1: i64, c: i64) -> i64 { 49 let fb: *i64 = (base + O_FB) as *i64 50 var yy: i64 = y0 51 if yy < 0 { yy = 0 } 52 var ye: i64 = y1 53 if ye > H { ye = H } 54 while yy < ye { 55 var xx: i64 = x0 56 if xx < 0 { xx = 0 } 57 var xe: i64 = x1 58 if xe > W { xe = W } 59 while xx < xe { fb[yy*W + xx] = c; xx = xx + 1 } 60 yy = yy + 1 61 } 62 return 0 63} 64 65func st_get(base: i64, i: i64) -> i64 { let p: *i64 = (base + O_ST) as *i64; return p[i] } 66func st_put(base: i64, i: i64, v: i64) -> i64 { let p: *i64 = (base + O_ST) as *i64; p[i]=v; return 0 } 67func cur_get(base: i64, i: i64) -> i64 { let p: *i64 = (base + O_CUR) as *i64; return p[i] } 68func cur_put(base: i64, i: i64, v: i64) -> i64 { let p: *i64 = (base + O_CUR) as *i64; p[i]=v; return 0 } 69// a GIVEN is a clue the generator left in place; it is immutable for the player. 70func given(base: i64, i: i64) -> i64 { if sc_get(base+O_CORE, SC_G, i)!=0 { return 1 } return 0 } 71func soln(base: i64, i: i64) -> i64 { return sc_get(base+O_CORE, SC_SOL, i) } 72 73// ---- puzzle lifecycle ---- 74func load_puzzle(base: i64, clues: i64, seed: i64) -> i64 { 75 let c: i64 = sc_gen(base + O_CORE, clues, seed) 76 if c < 0 { return 0-1 } 77 var i: i64 = 0 78 while i<81 { cur_put(base, i, sc_get(base+O_CORE, SC_G, i)); i=i+1 } 79 st_put(base,S_SOLVED,0) 80 st_put(base,S_SHOW,0) 81 st_put(base,S_CLUES,c) 82 st_put(base,S_SEED,seed) 83 // select the first empty cell so a keyboard player can start immediately 84 var s: i64 = 0-1 85 i = 0 86 while i<81 { if given(base,i)==0 { if s<0 { s=i } } i=i+1 } 87 st_put(base,S_SEL,s) 88 return c 89} 90 91func filled(base: i64) -> i64 { 92 var n: i64 = 0 93 var i: i64 = 0 94 while i<81 { if cur_get(base,i)!=0 { n=n+1 } i=i+1 } 95 return n 96} 97func wrong(base: i64) -> i64 { 98 var n: i64 = 0 99 var i: i64 = 0 100 while i<81 { 101 let v: i64 = cur_get(base,i) 102 if v!=0 { if v!=soln(base,i) { n=n+1 } } 103 i=i+1 104 } 105 return n 106} 107func check_solved(base: i64) -> i64 { 108 if filled(base)!=81 { st_put(base,S_SOLVED,0); return 0 } 109 if wrong(base)!=0 { st_put(base,S_SOLVED,0); return 0 } 110 st_put(base,S_SOLVED,1) 111 return 1 112} 113 114// ---- render ---- 115func render_impl(base: i64) -> i64 { 116 let fb: *i64 = (base + O_FB) as *i64 117 let mask: *u8 = (base + O_MSK) as *u8 118 let bg: i64 = rgb(11,15,20) 119 let cellbg: i64 = rgb(14,23,34) 120 let givbg: i64 = rgb(11,18,25) 121 let selbg: i64 = rgb(27,51,70) 122 let thin: i64 = rgb(36,49,64) 123 let thick: i64 = rgb(138,160,180) 124 let ink: i64 = rgb(230,237,243) 125 let mut: i64 = rgb(138,160,180) 126 let bad: i64 = rgb(248,81,73) 127 let good: i64 = rgb(63,185,80) 128 129 fillr(base, 0, 0, W, H, bg) 130 let sel: i64 = st_get(base,S_SEL) 131 let show: i64 = st_get(base,S_SHOW) 132 133 var r: i64 = 0 134 while r<9 { 135 var c: i64 = 0 136 while c<9 { 137 let i: i64 = r*9+c 138 let x: i64 = MARGIN + c*CELL 139 let y: i64 = MARGIN + r*CELL 140 var back: i64 = cellbg 141 if given(base,i)==1 { back = givbg } 142 if i==sel { back = selbg } 143 fillr(base, x+1, y+1, x+CELL, y+CELL, back) 144 let v: i64 = cur_get(base,i) 145 if v!=0 { 146 var col: i64 = ink 147 if given(base,i)==1 { col = mut } 148 if show==1 { if given(base,i)==0 { if v!=soln(base,i) { col = bad } } } 149 if st_get(base,S_SOLVED)==1 { col = good } 150 let sp: *u8 = (base + O_STR) as *u8 151 sp[0] = (48+v) as u8 152 let ch: i64 = 18 153 let adv: i64 = nf_adv_px(ch) 154 let tx: i64 = x + (CELL - adv)/2 155 let ty: i64 = y + (CELL - ch)/2 156 nf_draw_text_mem(fb, W, H, mask, tx, ty, ch, sp, 1, col) 157 } 158 c = c+1 159 } 160 r = r+1 161 } 162 // grid rules: thin every cell, THICK on box boundaries so the 3x3 structure reads at a glance 163 var k: i64 = 0 164 while k<10 { 165 let p: i64 = MARGIN + k*CELL 166 var col: i64 = thin 167 var wd: i64 = 1 168 if k%3==0 { col = thick; wd = 2 } 169 fillr(base, MARGIN, p, MARGIN+9*CELL, p+wd, col) 170 fillr(base, p, MARGIN, p+wd, MARGIN+9*CELL, col) 171 k = k+1 172 } 173 return 0 174} 175 176// ---- lifecycle / input ---- 177func init_impl(base: i64) -> i64 { 178 // 33 clues = a medium puzzle. The seed is fixed so a native run and a browser run of the same 179 // build produce the SAME first puzzle, which is what makes the gate's assertions reproducible. 180 let c: i64 = load_puzzle(base, 33, O_MAGIC_20260810) 181 render_impl(base) 182 return c 183} 184 185// k: 1..9 place, 0 erase, 10 left, 11 right, 12 up, 13 down, 20 new puzzle, 21 check. 186// returns 1 if the state changed. 187func tick_impl(base: i64, k: i64) -> i64 { 188 var changed: i64 = 0 189 if k==20 { 190 let nx: i64 = st_get(base,S_SEED) + O_MAGIC_7919 191 load_puzzle(base, 33, nx) 192 render_impl(base) 193 return 1 194 } 195 if k==21 { 196 st_put(base,S_SHOW,1) 197 render_impl(base) 198 return 1 199 } 200 var sel: i64 = st_get(base,S_SEL) 201 if k>=10 { 202 if k<=13 { 203 if sel<0 { sel=0 } 204 var r: i64 = sel/9 205 var c: i64 = sel-r*9 206 if k==10 { c=c-1; if c<0 { c=8 } } 207 if k==11 { c=c+1; if c>8 { c=0 } } 208 if k==12 { r=r-1; if r<0 { r=8 } } 209 if k==13 { r=r+1; if r>8 { r=0 } } 210 st_put(base,S_SEL, r*9+c) 211 render_impl(base) 212 return 1 213 } 214 } 215 if k>=0 { 216 if k<=9 { 217 if sel>=0 { 218 // a given clue is immutable: the player may never overwrite the puzzle's own evidence 219 if given(base,sel)==0 { 220 cur_put(base, sel, k) 221 check_solved(base) 222 changed = 1 223 } 224 } 225 } 226 } 227 if changed==1 { render_impl(base) } 228 return changed 229} 230 231// select the cell under a framebuffer pixel. Touch is the primary input on a phone, so cell hit 232// testing belongs in the module rather than in the page: the page should not need to know the grid 233// geometry (MARGIN/CELL) at all, or it becomes a second place that must be kept in sync. 234// returns the selected cell 0..80, or -1 when the point is outside the grid. 235func tap_impl(base: i64, px: i64, py: i64) -> i64 { 236 let gx: i64 = px - MARGIN 237 let gy: i64 = py - MARGIN 238 if gx < 0 { return 0-1 } 239 if gy < 0 { return 0-1 } 240 let c: i64 = gx/CELL 241 let r: i64 = gy/CELL 242 if c > 8 { return 0-1 } 243 if r > 8 { return 0-1 } 244 let i: i64 = r*9 + c 245 st_put(base, S_SEL, i) 246 render_impl(base) 247 return i 248} 249 250// score = correctly filled cells the player supplied (givens excluded), so it rises only on progress. 251func score_impl(base: i64) -> i64 { 252 var n: i64 = 0 253 var i: i64 = 0 254 while i<81 { 255 if given(base,i)==0 { 256 let v: i64 = cur_get(base,i) 257 if v!=0 { if v==soln(base,i) { n=n+1 } } 258 } 259 i=i+1 260 } 261 return n 262} 263 264// ---- the wasm export surface (base = 0 inside the module) ---- 265func init() -> i64 { return init_impl(0) } 266func tick(k: i64) -> i64 { return tick_impl(0, k) } 267func tap(px: i64, py: i64) -> i64 { return tap_impl(0, px, py) } 268func render() -> i64 { return render_impl(0) } 269func score() -> i64 { return score_impl(0) } 270func solved() -> i64 { return st_get(0, S_SOLVED) } 271func clues() -> i64 { return st_get(0, S_CLUES) } 272func fb_off() -> i64 { return O_FB } 273func ww() -> i64 { return W } 274func hh() -> i64 { return H } 275func mem_end() -> i64 { return O_END }