# The Lantern Hunt: how it is made

Six lanterns hidden on one region of the Ruler Alps, each on a prize column of the ruler sequence. The page draws a map, writes a clue for each lantern from the arithmetic, and lets the player tap the map or type a warp address. Everything the game knows comes from sitoolz.

## What it imports

```html
<script src="../../../sitoolz/js/rangeblends.js"></script>   <!-- the indexed hash and the river network -->
<script src="../../../sitoolz/js/rulerland.js"></script>     <!-- RulerLand: sqrem, warpDecode, the laws -->
<script src="../../../sitoolz/js/terrain.js"></script>       <!-- SITerrain: height, colour, prizes, lane -->
<script src="../../../sitoolz/js/citylaw.js"></script>       <!-- CityLaw: sitesIn, the settlements -->
<script src="../../../sitoolz/js/blake2b.js"></script>
<script src="../../../sitoolz/js/indexrng.js"></script>      <!-- SIIndexrng: the hunt's draw from a word -->
```

Functions used: `SITerrain.height(world, x, z)` and `SITerrain.colour(h, water, top)` for the map, `SITerrain.prizes(world, m)` for the region's prize columns (with x, z, h and warp code each) and its gate, `CityLaw.sitesIn(G0, a, b)` for the settlements a clue points at, `RulerLand.sqrem(g)` for the square remainder in the clue, `RulerLand.warpDecode(code)` to read a typed address, and `new SIIndexrng.Index(word).shuffle(...)` to pick six lanterns from a word, the same six on every machine.

## The twenty lines that make it

```js
const T = SITerrain, R = RulerLand, C = CityLaw, WORLD = "ruler-alps", G0 = T.G0, REGION = T.REGION;
// the map: every 8th column and 16th lane of region 0, a colour class each
const heights = [], classes = [];
for (let x = 0; x < REGION; x += 8) for (let z = -520; z <= 520; z += 16){ const h = T.height(WORLD, x, z); heights.push(h); classes.push(T.colour(h, 22, 160)); }
// the lanterns: six of the region's prize columns, drawn by a word
const region = T.prizes(WORLD, 0), sites = C.sitesIn(G0, 0, REGION - 1);
const order = new SIIndexrng.Index("lantern").shuffle("hunt", 0, region.prizes.map((_, i) => i));
const lanterns = order.slice(0, 6).map(i => region.prizes[i]).sort((a, b) => a.x - b.x);
// a clue from the arithmetic: the rank, the square remainder, the nearest settlement
const clue = L => { const s = sites.reduce((b, s) => Math.abs(s.x - L.x) < Math.abs(b.x - L.x) ? s : b);
  return `rank ${L.k}: one every ${1 << L.k} columns; square remainder ${R.sqrem(L.g)}, even; ${Math.abs(s.x - L.x)} columns ${s.x < L.x ? "east" : "west"} of the ${s.kind} at ${s.x}`; };
// a guess lights the nearest unlit lantern within 24 columns
const guess = (x, z) => { const L = lanterns.filter(l => !l.lit).sort((a, b) => Math.abs(a.x - x) - Math.abs(b.x - x))[0]; if (L && Math.abs(L.x - x) <= 24) L.lit = true; return L; };
// a warp address is world-column-lane-check in base 36; the world's own name carries a hyphen, so split from the right
const decode = code => { const p = code.split("-"), d = R.warpDecode("w-" + p.slice(-3).join("-")); return d && {world: p.slice(0, -3).join("-"), g: d.g, z: d.z}; };
const byCode = text => { const d = decode(text); if (d && d.world === WORLD) guess(d.g - G0, d.z); };
```

## How to change the law

- **Another range**: set `WORLD` to any key of `SITerrain.WORLDS` (`ruler-hills`, `ziggurats`, `dyadic-isles`, `light-cone`, …). The map, the prizes and the lanes follow; the Dyadic Downs put prizes on lattice lanes, the Ladder Range on shelves.
- **Harder or easier**: `NEAR` (24 columns) is the lighting radius. Prizes come every 32 columns at rank 5, so a radius over 16 can light a neighbour; drop it to 8 for a hunt that needs the exact column.
- **More lanterns**: the six come from `region.prizes` (73 in region 0 of the Alps); take 12 and the sixth-of-the-region rule spreads them two to a slot.
- **Another region**: `T.prizes(WORLD, m)` for region `m`; the map then samples `x` from `m*REGION`.

Example: `const WORLD = "ruler-hills"` gives a greener map (base 28, top 260) and its prizes on the hills' seven lanes.

## How to verify it

The arithmetic the game stands on is checked by the fixture and twin tests in sitoolz:

- `fixtures/ruler-land.json` and `tests/twins.test.cjs`: `prizeColumns`, `sqrem`, `warpCode`/`warpDecode`, the heights of the landscapes.
- `fixtures/terrain.json` and `tests/twins-terrain.test.cjs`: `SITerrain.height`, `colour`, `lane` and `prizes` on 14 worlds against the Python.
- `fixtures/city-law.json` and `tests/twins-city.test.cjs`: `sitesIn` against the Python's brute-force census.
- `fixtures/indexrng.json` and `tests/twins-indexrng.test.cjs`: `Index.shuffle` is the same on both sides, so the hunt for a word is the same everywhere.

```
cd ~/mywork/sitoolz && npm test && python3 tests/test_sitoolz.py
node demo/lantern-hunt/smoke.cjs      # from sitoolz-showcase: plays two lanterns and photographs the page
```

## What was missing

- `RulerLand.warpDecode` (and `rulerland.warp_decode` in Python) split an address on every hyphen, so a world whose name carries a hyphen (`ruler-alps`, every key of `SITerrain.WORLDS`) could not be decoded although `SITerrain.prizes` wrote the code. Found by this game; fixed in both twins the same day (they now split from the right, since the check digit depends on the column and lane alone), and `fixtures/ruler-land.json` carries two hyphenated addresses so it stays fixed.
- Nothing else: the map, the prizes, the settlements, the remainder and the draw all came from the kit.

![](shot.png)
