code wiki / _hdl_build / nx_sudoku.nx
nx_sudoku.nx source
↩ module page · 318 lines · 14988 B
1// nx_sudoku.nx -- the FILE SURFACE for sudoku: proven-unique puzzle banks in the estate's own format.
2//
3// THIS FILE HOLDS NO ALGORITHM. Rules, solver, solution counter and carver live in nx_sudoku_core.nx
4// and are shared with nx_sudoku_wasm (the playable module). An earlier version of this file carried its
5// own sd_ok/sd_fill/sd_count/sd_carve, which meant TWO implementations of one ruleset in the estate --
6// the duplicate-ruler defect. The extraction was only half the fix; deleting this copy is the rest.
7// Verified by re-banking seed 1101 and comparing bytes: a refactor that reproduces the banked artifact
8// exactly has provably changed no behaviour.
9//
10// NATIVE BANK FORMAT. The bank is a sovereign nx_gamesave state file, NOT a TSV. TSV has no header, no
11// version, no checksum and no atomicity: a torn write or a flipped byte reads back as a plausible
12// puzzle. gs_save gives the estate's guarantees for free -- 48-byte versioned header, rolling checksum
13// with CORRUPTION REFUSED LOUD, a future format REFUSED rather than misparsed, atomic tmp+fsync+rename,
14// and additive .prev banking (rule 13).
15// layout: 162 i64 per puzzle -- [0..80] the puzzle digits, [81..161] its solution.
16//
17// UNIQUENESS IS COUNTED, NEVER ASSUMED: a clue is removed only while exactly one solution survives, and
18// every row is re-counted before it reaches the file. The clue floor is 17 -- McGuire, Tugemann &
19// Civario (2012) proved no 16-clue sudoku has a unique solution, so fewer is CLAMPED, not honoured.
20//
21// usage: nx_sudoku --kat self-test -> VERDICT=GREEN/RED, exit carries it
22// nx_sudoku bank <count> <clues> <seed> <out.sav> the native, checksummed bank
23// nx_sudoku jsbank <in.sav> <out.js> derive the page fragment FROM that bank
24// nx_sudoku gen <count> <clues> <seed> text emit, for eyeballing a grid
25// exit : 0 ok | 1 RED/refused | 2 usage
26// license_tier: ORIGINAL expect_exit: 0
27import "nx_syscalls.nx"
28import "nx_gamesave.nx"
29import "nx_sudoku_core.nx"
30const SD_MAGIC_12345: i64 = 12345
31const SD_MAGIC_4242: i64 = 4242
32
33const SD_SCHEMA: i64 = 5211 // sudoku bank v1 schema id, checked on load
34const SD_CELLS: i64 = 162 // 81 puzzle + 81 solution
35
36// ---------- io ----------
37func sd_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
38func sd_pn(v: i64) -> i64 {
39 let t: *u8=sys_mmap(28); var m: i64=v
40 if m<0 { sys_write(1,"-" as *u8,1); m=0-m }
41 var k: i64=0
42 if m==0 { t[0]=48 as u8; k=1 }
43 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
44 let b: *u8=sys_mmap(28); var i: i64=0
45 while i<k { b[i]=t[k-1-i]; i=i+1 }
46 sys_write(1,b,k); return 0
47}
48func sd_cat(dst: *u8, off: i64, s: *u8) -> i64 {
49 var o: i64=off; var i: i64=0
50 while s[i]!=(0 as u8) { dst[o]=s[i]; o=o+1; i=i+1 }
51 return o
52}
53func sd_atoi(s: *u8) -> i64 {
54 var v: i64=0; var i: i64=0
55 while s[i]!=(0 as u8) {
56 let c: i64 = s[i] as i64
57 if c>=48 { if c<=57 { v=v*10+(c-48) } }
58 i=i+1
59 }
60 return v
61}
62
63// generate `cnt` proven-unique puzzles into cells[] (SD_CELLS i64 each). returns puzzles made.
64// The PRNG is seeded ONCE and the stream continues across puzzles -- seeding per puzzle would emit the
65// same grid `cnt` times. This is why the loop calls sc_fill/sc_carve directly rather than sc_gen.
66func sd_fill_bank(cells: *i64, cnt: i64, clu: i64, seed: i64) -> i64 {
67 let cb: i64 = sys_mmap(SC_SIZE) as i64
68 sc_seed(cb, seed)
69 var made: i64 = 0
70 var run: i64 = 1
71 while run == 1 {
72 if made >= cnt { run = 0 } else {
73 sc_clear(cb, SC_G)
74 if sc_fill(cb, SC_G, 0) == 1 {
75 sc_copy(cb, SC_G, SC_SOL)
76 sc_carve(cb, clu)
77 // REFUSE to bank a row we have not just proven unique. carve already guarantees it;
78 // re-checking means a silent lie cannot reach the file.
79 sc_copy(cb, SC_G, SC_WORK)
80 if sc_count(cb, SC_WORK, 0, 0) == 1 {
81 let b: i64 = made*SD_CELLS
82 var i: i64 = 0
83 while i < 81 {
84 cells[b+i] = sc_get(cb, SC_G, i)
85 cells[b+81+i] = sc_get(cb, SC_SOL, i)
86 i = i + 1
87 }
88 made = made + 1
89 }
90 } else { run = 0 }
91 }
92 }
93 return made
94}
95
96// ---------- self-test ----------
97func sd_selftest() -> i64 {
98 var f: i64 = 0
99 let cb: i64 = sys_mmap(SC_SIZE) as i64
100 let quad: *i64 = sys_mmap(64) as *i64
101
102 // ---- a complete grid is produced and is internally legal ----
103 sc_seed(cb, SD_MAGIC_12345)
104 sc_clear(cb, SC_G)
105 if sc_fill(cb, SC_G, 0)!=1 { f=f+1 }
106 if sc_clues(cb, SC_G)!=81 { f=f+1 }
107 sc_copy(cb, SC_G, SC_SOL)
108 // legality: lift each cell and confirm its own value is still placeable
109 var i: i64 = 0
110 while i<81 {
111 let v: i64 = sc_get(cb, SC_G, i)
112 sc_put(cb, SC_G, i, 0)
113 if sc_ok(cb, SC_G, i, v)!=1 { f=f+1 }
114 sc_put(cb, SC_G, i, v)
115 i=i+1
116 }
117 // a SOLVED grid has exactly one solution
118 sc_copy(cb, SC_G, SC_WORK)
119 if sc_count(cb, SC_WORK, 0, 0)!=1 { f=f+1 }
120
121 // ---- NEG-CONTROL 1: the empty grid has many solutions (the counter is not stuck at 1) ----
122 sc_clear(cb, SC_WORK)
123 if sc_count(cb, SC_WORK, 0, 0)<2 { f=f+1 }
124
125 // ---- NEG-CONTROL 2: a PLANTED unavoidable set must be detected as non-unique ----
126 if sc_unavoidable(cb, SC_SOL, quad)!=1 { f=f+1 } else {
127 sc_copy(cb, SC_SOL, SC_WORK)
128 sc_put(cb, SC_WORK, quad[0], 0); sc_put(cb, SC_WORK, quad[1], 0)
129 sc_put(cb, SC_WORK, quad[2], 0); sc_put(cb, SC_WORK, quad[3], 0)
130 // assert the fixture REACHED the condition before asserting the outcome
131 if sc_get(cb, SC_WORK, quad[0])!=0 { f=f+1 }
132 if sc_count(cb, SC_WORK, 0, 0)<2 { f=f+1 }
133 }
134
135 // ---- POSITIVE CONTROL: a carved puzzle is unique, respects the floor, and is a subset ----
136 sc_copy(cb, SC_SOL, SC_G)
137 sc_seed(cb, 999)
138 let left: i64 = sc_carve(cb, 30)
139 if left<SC_CLUE_FLOOR { f=f+1 }
140 if sc_clues(cb, SC_G)!=left { f=f+1 }
141 sc_copy(cb, SC_G, SC_WORK)
142 if sc_count(cb, SC_WORK, 0, 0)!=1 { f=f+1 }
143 i = 0
144 while i<81 {
145 let g: i64 = sc_get(cb, SC_G, i)
146 if g!=0 { if g!=sc_get(cb, SC_SOL, i) { f=f+1 } }
147 i=i+1
148 }
149 // and it must actually have holes (a "puzzle" of 81 clues is not a puzzle)
150 if left>=81 { f=f+1 }
151
152 // ---- the 17-clue floor is CLAMPED, not silently honoured ----
153 sc_copy(cb, SC_SOL, SC_G)
154 sc_seed(cb, SD_MAGIC_4242)
155 if sc_carve(cb, 5)<SC_CLUE_FLOOR { f=f+1 }
156
157 // ---- DETERMINISM: same seed => identical grid ----
158 let cb2: i64 = sys_mmap(SC_SIZE) as i64
159 sc_seed(cb2, SD_MAGIC_12345)
160 sc_clear(cb2, SC_G)
161 if sc_fill(cb2, SC_G, 0)!=1 { f=f+1 }
162 i = 0
163 while i<81 {
164 if sc_get(cb2, SC_G, i)!=sc_get(cb, SC_SOL, i) { f=f+1; i=81 } else { i=i+1 }
165 }
166
167 // ---- NATIVE BANK ROUND-TRIP: gs_save -> gs_load is bit-exact, rows still provably unique ----
168 let bc: *i64 = sys_mmap(3*SD_CELLS*8) as *i64
169 let made: i64 = sd_fill_bank(bc, 3, 34, 777)
170 if made!=3 { f=f+1 }
171 let bp: *u8 = "/tmp/nx_sudoku_kat_bank.sav" as *u8
172 if gs_save(bp, SD_SCHEMA, bc, made*SD_CELLS, 0) < 0 { f=f+1 }
173 if gs_verify(bp) < 0 { f=f+1 }
174 let rb: *i64 = sys_mmap(3*SD_CELLS*8) as *i64
175 let meta: *i64 = sys_mmap(64) as *i64
176 let got: i64 = gs_load(bp, rb, 3*SD_CELLS, meta)
177 if got != made*SD_CELLS { f=f+1 }
178 if got <= 0 { f=f+1 } else {
179 var d: i64 = 0
180 while d < got { if rb[d]!=bc[d] { f=f+1; d=got } else { d=d+1 } }
181 var p: i64 = 0
182 while p < made {
183 let b: i64 = p*SD_CELLS
184 var q: i64 = 0
185 while q < 81 { sc_put(cb, SC_WORK, q, rb[b+q]); q=q+1 }
186 if sc_count(cb, SC_WORK, 0, 0)!=1 { f=f+1 }
187 q = 0
188 while q < 81 { if rb[b+q]!=0 { if rb[b+q]!=rb[b+81+q] { f=f+1 } } q=q+1 }
189 p = p + 1
190 }
191 }
192 return f
193}
194
195// ---------- emit ----------
196func sd_emit_row(p: *i64, s: *i64) -> i64 {
197 let line: *u8 = sys_mmap(200)
198 var o: i64 = 0
199 var i: i64 = 0
200 while i<81 { line[o]=(48+p[i]) as u8; o=o+1; i=i+1 }
201 line[o]=9 as u8; o=o+1
202 i=0
203 while i<81 { line[o]=(48+s[i]) as u8; o=o+1; i=i+1 }
204 line[o]=10 as u8; o=o+1
205 sys_write(1,line,o)
206 return 0
207}
208
209func main(argc: i64, argv: *i64) -> i64 {
210 if argc>=2 {
211 let a1: *u8 = argv[1] as *u8
212 if a1[0]==(45 as u8) {
213 sd_w("=== nx_sudoku --kat (uniqueness COUNTED; 17-clue floor = McGuire/Tugemann/Civario 2012) ===\n" as *u8)
214 sd_w("algorithm: nx_sudoku_core (shared with nx_sudoku_wasm -- ONE implementation in the estate)\n" as *u8)
215 let f: i64 = sd_selftest()
216 sd_w("checks failed=" as *u8); sd_pn(f)
217 sd_w(" (complete-fill + legality + solved-is-unique + neg-control-empty + neg-control-planted-unavoidable-set + carve-unique + clue-floor-clamp + subset + determinism + native-bank-round-trip)\n" as *u8)
218 if f==0 { sd_w("VERDICT=GREEN\n" as *u8); sys_exit(0); return 0 }
219 sd_w("VERDICT=RED\n" as *u8); sys_exit(1); return 1
220 }
221 // bank <count> <clues> <seed> <out.sav>
222 if a1[0]==(98 as u8) {
223 if argc>=6 {
224 let cnt: i64 = sd_atoi(argv[2] as *u8)
225 let clu: i64 = sd_atoi(argv[3] as *u8)
226 let sd: i64 = sd_atoi(argv[4] as *u8)
227 if cnt<=0 { sd_w("BANK-REFUSED count must be > 0\n" as *u8); sys_exit(2); return 2 }
228 let cells: *i64 = sys_mmap(cnt*SD_CELLS*8) as *i64
229 let made: i64 = sd_fill_bank(cells, cnt, clu, sd)
230 if made!=cnt {
231 sd_w("BANK-REFUSED generated " as *u8); sd_pn(made)
232 sd_w(" of " as *u8); sd_pn(cnt); sd_w(" -- nothing written\n" as *u8)
233 sys_exit(1); return 1
234 }
235 // saved_at is CALLER-SUPPLIED 0 on purpose: the bank stays byte-reproducible for a
236 // fixed (count, clues, seed), so a rebuild can be compared to the banked bytes.
237 let rc: i64 = gs_save(argv[5] as *u8, SD_SCHEMA, cells, made*SD_CELLS, 0)
238 if rc<0 { sd_w("BANK-FAIL gs_save rc=" as *u8); sd_pn(rc); sd_w("\n" as *u8); sys_exit(1); return 1 }
239 sd_w("BANK-OK puzzles=" as *u8); sd_pn(made)
240 sd_w(" fields=" as *u8); sd_pn(made*SD_CELLS)
241 sd_w(" schema=" as *u8); sd_pn(SD_SCHEMA)
242 sd_w(" path=" as *u8); sd_w(argv[5] as *u8); sd_w("\n" as *u8)
243 sys_exit(0); return 0
244 }
245 sd_w("usage: nx_sudoku bank <count> <clues> <seed> <out.sav>\n" as *u8)
246 sys_exit(2); return 2
247 }
248 // jsbank <in.sav> <out.js> -- DERIVE the page's script fragment FROM the native store.
249 // The .sav is authoritative; this verb is the only reader, so the page parses no data format
250 // and there is exactly one definition of the bank layout. gs_load REFUSES a corrupt or
251 // future-version file, so a damaged bank cannot silently become a page with wrong puzzles.
252 if a1[0]==(106 as u8) {
253 if argc>=4 {
254 let cap: i64 = GS_MAXFIELDS
255 let cells: *i64 = sys_mmap(cap*8) as *i64
256 let meta: *i64 = sys_mmap(64) as *i64
257 let n: i64 = gs_load(argv[2] as *u8, cells, cap, meta)
258 if n<0 { sd_w("JSBANK-REFUSED gs_load rc=" as *u8); sd_pn(n); sd_w(" (" as *u8); sd_w(gs_errname(n)); sd_w(") -- nothing written\n" as *u8); sys_exit(1); return 1 }
259 if n==0 { sd_w("JSBANK-REFUSED bank is empty -- nothing written\n" as *u8); sys_exit(1); return 1 }
260 if n%SD_CELLS!=0 { sd_w("JSBANK-REFUSED field count " as *u8); sd_pn(n); sd_w(" is not a multiple of 162 -- not a sudoku bank\n" as *u8); sys_exit(1); return 1 }
261 let pz: i64 = n/SD_CELLS
262 let out: *u8 = sys_mmap(pz*180+256)
263 var o: i64 = 0
264 // A GENERATED ARTEFACT MUST DECLARE ITSELF, or a later reader consumes it as authored data.
265 o = sd_cat(out, o, "/* NX-DERIVED: generated by nx_sudoku jsbank from a checksummed nx_gamesave bank. Do not edit. */\n" as *u8)
266 o = sd_cat(out, o, "var SBANK=[" as *u8)
267 var p: i64 = 0
268 while p < pz {
269 if p>0 { out[o]=44 as u8; o=o+1 }
270 let b: i64 = p*SD_CELLS
271 out[o]=39 as u8; o=o+1
272 var i: i64 = 0
273 while i<81 { out[o]=(48+cells[b+i]) as u8; o=o+1; i=i+1 }
274 out[o]=124 as u8; o=o+1
275 i=0
276 while i<81 { out[o]=(48+cells[b+81+i]) as u8; o=o+1; i=i+1 }
277 out[o]=39 as u8; o=o+1
278 p=p+1
279 }
280 o = sd_cat(out, o, "];\n" as *u8)
281 let fd: i64 = sys_openat_wr(argv[3] as *u8, 0x1a4)
282 if fd<0 { sd_w("JSBANK-FAIL cannot open output\n" as *u8); sys_exit(1); return 1 }
283 sys_write(fd, out, o)
284 sys_close(fd)
285 sd_w("JSBANK-OK puzzles=" as *u8); sd_pn(pz)
286 sd_w(" bytes=" as *u8); sd_pn(o)
287 sd_w(" from=" as *u8); sd_w(argv[2] as *u8); sd_w("\n" as *u8)
288 sys_exit(0); return 0
289 }
290 sd_w("usage: nx_sudoku jsbank <in.sav> <out.js>\n" as *u8)
291 sys_exit(2); return 2
292 }
293 // gen <count> <clues> <seed> -- text emit, kept for eyeballing a grid
294 if argc>=5 {
295 let cnt: i64 = sd_atoi(argv[2] as *u8)
296 let clu: i64 = sd_atoi(argv[3] as *u8)
297 let sd: i64 = sd_atoi(argv[4] as *u8)
298 if cnt<=0 { sd_w("GEN-REFUSED count must be > 0\n" as *u8); sys_exit(2); return 2 }
299 let cells: *i64 = sys_mmap(cnt*SD_CELLS*8) as *i64
300 let made: i64 = sd_fill_bank(cells, cnt, clu, sd)
301 var p: i64 = 0
302 let pz: *i64 = sys_mmap(81*8) as *i64
303 let sz: *i64 = sys_mmap(81*8) as *i64
304 while p<made {
305 let b: i64 = p*SD_CELLS
306 var i: i64 = 0
307 while i<81 { pz[i]=cells[b+i]; sz[i]=cells[b+81+i]; i=i+1 }
308 sd_emit_row(pz, sz)
309 p=p+1
310 }
311 sys_exit(0); return 0
312 }
313 sd_w("usage: nx_sudoku --kat | bank <count> <clues> <seed> <out.sav> | jsbank <in.sav> <out.js> | gen <count> <clues> <seed>\n" as *u8)
314 sys_exit(2); return 2
315 }
316 sd_w("usage: nx_sudoku --kat | bank <count> <clues> <seed> <out.sav> | jsbank <in.sav> <out.js> | gen <count> <clues> <seed>\n" as *u8)
317 sys_exit(2); return 2
318}