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 }