# Sudoku Town: how it was built

A playable Sudoku whose puzzles are Larsdoku's, with a hint from the singles solver, a check against the
solution, a "watch Larsdoku solve it" mode that steps through the proven path in WSRF Sudoku Path
Notation, and the town the solution makes, coloured by the terrain engine. Lars Rocha's puzzles,
solver and notation; the page only draws and listens. Play it at `index.html`; `node smoke.cjs` drives
it headless.

## What it imports

```html
<script src="../../../sitoolz/js/rangeblends.js"></script>
<script src="../../../sitoolz/js/rulerland.js"></script>
<script src="../../../sitoolz/js/terrain.js"></script>      <!-- SITerrain.WORLDS["sudoku-mesas"], SITerrain.colour -->
<script src="../../../sitoolz/js/puzzles.js"></script>      <!-- SIPuzzles: norm, peers, clues, candidates, isSolved, fits, solveSingles -->
<script src="../../../sitoolz/js/spn.data.js"></script>
<script src="../../../sitoolz/js/spn.js"></script>          <!-- SISpn: parse, notation, annotation, techSummary -->
```

The puzzles come from `fetch("../../../sitoolz/fixtures/puzzles.json")`: `grids` are Wander's eight Sudoku
towns and one easy grid with Larsdoku's solutions, `forged` are three minimal puzzles from Larsdoku's forge,
and `paths` are three solves written in the notation (the easy grid and two forged ones), which the page
offers as "Watch Larsdoku solve it".

## The twenty lines that make it

```js
const P = SIPuzzles, SPN = SISpn, T = SITerrain, MESA = T.WORLDS["sudoku-mesas"];
let puzzle = P.norm(fx.grids[8].puzzle), solution = fx.grids[8].solution, grid = puzzle;

function place(cell, digit){ const g = grid.split(""); g[cell] = String(digit); grid = g.join(""); }
function hint(){ const step = P.solveSingles(grid).steps[0];          // the first naked or hidden single
  if (step) place(step.cell, step.digit); else place(grid.indexOf("0"), +solution[grid.indexOf("0")]); }
function check(){ return [...grid].map((d, c) => d !== "0" && d !== solution[c]); }   // which cells disagree
function won(){ return P.isSolved(grid) && P.fits(puzzle, grid); }

const path = SPN.parse(fx.paths[0].spn);                              // Larsdoku's proven moves
function stepTo(n){ const g = puzzle.split("");                       // replay the first n placements
  for (const m of path.moves.slice(0, n)) if (m.type === "placement") g[(+m.cell[1] - 1)*9 + (+m.cell[3] - 1)] = String(m.digit);
  grid = g.join(""); const m = path.moves[n - 1]; return m ? SPN.notation(m) + "  " + SPN.annotation(m) + "  " + m.reasoning : ""; }

function townColour(cell){                                            // the Sudoku Mesas' law: a plateau per digit
  const h = MESA.base + (+solution[cell])*MESA.args.unit; return T.colour(h, MESA.water, MESA.top); }   // 0 water … 5 snow
```

The rest of `game.js` is the board, the keypad, pencil marks (a `Set` per cell, cleared by `peers` when a
digit lands), keyboard and touch, and the canvas that paints the town.

## How to change the law

- **Another puzzle set**: point the fetch at any list of 81-character grids; `P.clues` and `P.solveFull`
  give the clue count and a solution for one that has none recorded.
- **A stronger hint**: `P.solveSingles` stops at the first two levels. To hint from a proven path
  instead, parse the puzzle's `.spn` (written by `sitoolz.puzzles.path` in Python) and take the next
  placement the path makes, with `m.reasoning` as the explanation.
- **A different town**: the plateau height is `MESA.base + digit * MESA.args.unit` with the colour bands
  of `T.colour(h, water, top)`. Change `unit` to 8 and the nines reach the snow line; change `water` to
  20 and the ones and twos drown.
- **Forge new puzzles**: in Python, `sitoolz.puzzles.forge(seed)` gives a minimal puzzle and its solution;
  `sitoolz.puzzles.path(puzzle)` writes its proven path; add both to the fixture and the page offers them.

## How to verify it

`fixtures/puzzles.json` is written by `tests/write_puzzles_fixture.py` from the Python side (Larsdoku's
solves, the singles solver's steps, the forged puzzles, the notation), and `tests/twins-puzzles.test.cjs`
checks that `js/puzzles.js` and `js/spn.js` reproduce every one of them: 79 checks, the notation parsed,
replayed to the solution and rewritten byte for byte. `npm test` in `sitoolz` runs it; `node smoke.cjs`
here drives the page through three hints, a check, a hand move and the whole path.

## What was missing

Nothing. Two notes: the singles solver cannot finish most of the towns (they need Larsdoku's higher
techniques), so a hint past the singles falls back to the recorded solution and says so; and Larsdoku's
`crossHatch` technique is written in the notation as `HiddenSingle`, which is what it is.

![](shot.png)
