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}