# The Square Lanterns, from the kit

Light the lanterns whose numbers multiply to a perfect square. The original lives in the Range
Arcade; this one is rebuilt from `sitoolz` alone: the row is dealt by indexed randomness, the
split is judged by the ruler land's square remainder, and the hint is a null-space vector found
by the same GF(2) elimination Lars Logic agrees with.

## What it imports

```html
<script src="../../../sitoolz/js/rangeblends.js"></script>   <!-- rulerland needs it -->
<script src="../../../sitoolz/js/rulerland.js"></script>     <!-- RulerLand.sqrem, RulerLand.isqrt: the square split -->
<script src="../../../sitoolz/js/blake2b.js"></script>       <!-- indexrng's default backend -->
<script src="../../../sitoolz/js/qrand.js"></script>         <!-- indexrng's jacobi backend -->
<script src="../../../sitoolz/js/indexrng.js"></script>      <!-- SIIndexrng.Index(name).roll(stream, index, sides): the deal -->
<script src="../../../sitoolz/js/logic.js"></script>         <!-- SILogic.ConstraintSystem, SILogic.solve: the square set -->
```

Functions used: `new SIIndexrng.Index(name)` and `.roll("lanterns", index, sides)`;
`RulerLand.sqrem(n)` and `RulerLand.isqrt(n)`; `new SILogic.ConstraintSystem(n).addXor(vars, rhs)`
and `SILogic.solve(system)` (its `free` and `forced`).

## The twenty lines that make it

```js
const rng = new SIIndexrng.Index("row-2-0"), primes = [2, 3, 5, 7, 11, 13, 17];          // the same row on every machine
const numbers = [], rows = [];                                                            // rows[i] = the primes lantern i holds an odd number of times
for (let i = 0, t = 0; numbers.length < 9; t++){
  const exps = primes.map((p, c) => rng.roll("lanterns", (i*7 + c)*16 + t, p < 7 ? 4 : 3) - 1);   // exponents 0..2 (0..3 for 2, 3, 5)
  let n = 1, row = []; exps.forEach((e, c) => { if (e > 0){ n *= primes[c] ** e; if (e & 1) row.push(c); } });
  if (row.length && row.length <= 4 && n <= 20000){ numbers.push(n); rows.push(row); i++; }  // no bare squares, small enough to read
}
const lit = new Set();                                                                    // the player's choice
const product = () => [...lit].reduce((a, i) => a*BigInt(numbers[i]), 1n);
const won = () => { const n = Number(product()); return Number.isSafeInteger(n) ? RulerLand.sqrem(n) === 0 : bigSqrem(product()) === 0n; };
// the hint: one variable per lantern, one XOR row per prime, rhs 0; the null space is every square set
const base = new SILogic.ConstraintSystem(numbers.length);
primes.forEach((_, c) => base.addXor(rows.map((r, i) => r.includes(c) ? i : -1).filter(i => i >= 0), 0));
const free = SILogic.solve(base).free;                                                    // the choices; one exists because lanterns > primes
const cs = new SILogic.ConstraintSystem(numbers.length); cs.rows = base.rows.slice();
for (const f of free) cs.addXor([f], f === free[0] ? 1 : 0);                              // fix every free variable: now every variable is forced
const answer = Object.keys(SILogic.solve(cs).forced).filter(i => SILogic.solve(cs).forced[i] === 1).map(Number);
```

`bigSqrem` is the same split in BigInt (Newton from the float root) for products past 2^53,
which thirteen lanterns reach.

## How to change the law

- **The deal.** The name given to `SIIndexrng.Index` is the whole row: `row-<level>-<serial>`
  for the numbered rows, `daily-<date>` for today's. Change the name and every machine changes
  with you. The `sides` of each roll set the exponent range; `p < 7 ? 4 : 3` gives the small
  primes a chance at a cube, which is what makes `2³ · 7` glow as a 2-flame and a 7-flame.
- **The size.** `LEVELS` in `game.js` sets lanterns `n` and flame kinds `k` per row. Keep
  `n > k`: the system then has at least `n − k` free variables, so a square set always exists.
  With `n = k + 1` there is exactly one non-trivial square set (the hint's); with `n = k + 3`
  there are seven, and the hint shows the first.
- **The judge.** `RulerLand.sqrem` is the square split `v = r² + rem` of the ruler land (the
  same remainder that decides where prizes stand). Swap the win test for `rem % 2 === 0` and you
  have a different, much easier game: even remainders instead of zero ones.

Example: change `{name: "Dusk", n: 7, k: 5}` to `{name: "Dusk", n: 9, k: 5}` and the Dusk rows
get sixteen square sets each; press Hint twice on a fresh row and the two hints will agree,
because `squareSet(p, 0)` always picks the first free variable.

## How to verify it

- `sitoolz/fixtures/indexrng.json` + `tests/twins-indexrng.test.cjs`: `Index.roll` gives the
  same values in Python and JavaScript for the same seed, stream and index (138 checks), so a row
  dealt here is the row `sitoolz.indexrng.Index("row-2-0").roll("lanterns", i, sides)` deals in
  Python.
- `sitoolz/fixtures/logic.json` + `tests/twins-logic.test.cjs`: twenty-four systems solved by
  Lars Logic and by the JavaScript elimination agree on forced, free, rank and the dependent
  pivots. The hint is that elimination.
- `sitoolz/fixtures/ruler-land.json` + `tests/twins-ruler-land.test.cjs`: `sqrem` agrees on both
  sides (the prize law stands on it).
- `node smoke.cjs` here: five rows of 5 to 13 lanterns are solved by following the hints, and
  every product is checked to be a square by an independent BigInt root.

## What was missing

- `SILogic.solve` reported the free and forced variables but not a full solution, so a particular null-space vector could not be read straight off; the game fixes every free variable with extra unit rows and solves again. Found by this game; the library now has `logic.assignment(system, freeValues)` on both sides (a full solution with the free variables as you set them, so a null-space vector is one call: all free variables 0 but one), checked in `fixtures/logic.json`.
- `RulerLand.sqrem` took a Number, and the product of thirteen lanterns passes 2^53, so the page carries its own BigInt split. The library's `sqrem` and `isqrt` now take a BigInt as well (a BigInt in, a BigInt out), with five big remainders in `fixtures/ruler-land.json`.

![](shot.png)
