librarygames/lights-out/Lamps.xtl
Lights out's rules, a solver and the board's picture, a library: lights-out.xtl (scripted), play.xtl (the game: a board you click) and the web page all use these. The board is 25 lights, row by row, 1 on. A press toggles a light and its four neighbors: the press matrix, 25 by 25, says which, and a whole board of presses is one product with it, mod 2. A game's state: the moves made, the square a hint shows (0 none), then the 25 lights.
The press matrix
ˡp̲resses : (Num a, Truthy a) => Unit -> a
Which lights each press toggles: row i is square i's press, a 1 on every square at most one step away (itself and its four neighbors), all 625 pairs at once by two tables of distances.
ˡᵒ⁼u̲se< "Lamps"
5 5 r̲eshape 13 s̲elect ˡᵒp̲resses @ 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0
ˡp̲resses ← { @ → 0 + 1 ≥ (a̲bs row '− t̲able row) + a̲bs col '− t̲able col }
ˡa̲fter : Int -> Int -> Int
The board after pressing the squares p (a 0/1 vector of 25) on board b: each light flips once for every press that reaches it, mod 2, one inner product for all of them.
ˡa̲fter ← { b p → (b + p '+ '× i̲nner ˡp̲resses @) m̲od 2 }
Solving it
ʰs̲tep : Int -> Int -> Int
One column of Gaussian elimination mod 2 on the augmented matrix m (the press matrix, the board as its last column), rc the next pivot row and the column: a row with a 1 in the column is swapped up, and added (mod 2) to every other row with a 1 there; a column without one is a free press, left 0.
ʰs̲tep ← { m rc → r ← f̲irst rc c ← 2 s̲elect rc (c > 25) ∨ r > 25 ? m cand ← w̲here (1 = c s̲elect₂ m) ∧ (r̲ange 25) ≥ r 0 = t̲ally cand ? m ʰs̲tep r c̲at c + 1 p ← f̲irst cand k ← r̲ange 25 m ← (k + ((p − r) × k = r) + (r − p) × k = p) s̲elect m hit ← 0 + (1 = c s̲elect₂ m) ∧ k ≠ r m ← (m + hit '× t̲able r s̲elect m) m̲od 2 m ʰs̲tep (r + 1) c̲at c + 1 }
ˡs̲olve : Int -> Int
The presses that turn board b off (a 0/1 vector of 25), when there are any: the matrix reduced mod 2, each pivot's press read from the board's column; the free presses (5 by 5 has two) left 0.
ˡᵒ⁼u̲se< "Lamps"
b ← 25 r̲eshape 0 0 1 0 0
0 = '+ r̲/ b ˡᵒa̲fter ˡᵒs̲olve b 1
ˡs̲olve ← { b → m ← ((ˡp̲resses @) c̲at₂ b) ʰs̲tep 1 1 lead ← '{ i → f̲irst (w̲here 1 = 25 t̲ake i s̲elect m) c̲at 0 } e̲ach r̲ange 25 last ← 26 s̲elect₂ m '{ j → '+ r̲/ (lead = j) × last } e̲ach r̲ange 25 }
The game
ˡn̲ew : Any a => a -> Int
A new puzzle: some squares pressed at random on a board all off, so it can always be solved (a board left all off gets its middle pressed).
ˡn̲ew ← { x → p ← 0 + 1 = r̲oll! 25 r̲eshape 2 p ← p + (0 = '+ r̲/ p) × 13 = r̲ange 25 0 0 c̲at (25 r̲eshape 0) ˡa̲fter p }
ˡp̲ress : Int -> Int -> Int
Square k (1 to 25) pressed: one more move, the hint gone.
ˡp̲ress ← { s k → (1 + ˡm̲oves s) c̲at 0 c̲at (ˡl̲ights s) ˡa̲fter 0 + k = r̲ange 25 }
ˡh̲int : Int -> Int
A hint: the first square of a solution of the board as it is.
ˡh̲int ← { s → (ˡm̲oves s) c̲at (f̲irst (w̲here 1 = ˡs̲olve ˡl̲ights s) c̲at 0) c̲at ˡl̲ights s }
ˡs̲tatus : (Num a, Num b, Truthy b) => a -> b
0 playing, 1 every light off.
ˡs̲tatus ← { s → 0 + 0 = '+ r̲/ ˡl̲ights s }
The picture and the clicks
ˡs̲quares : Unit -> Float
The squares as rectangles (x, y, width, height), one row each, 1 to 25.
ˡs̲quares ← { @ → o̲\ (4 c̲at 25) r̲eshape (200.0 + 170 × f̲loat col) c̲at (130.0 + 170 × f̲loat row) c̲at (25 r̲eshape 160.0) c̲at 25 r̲eshape 160.0 }
ˡc̲lick : Int -> Float -> Int
A click at xy: a square presses it; the first button starts a new puzzle, the second shows a hint.
ˡc̲lick ← { s xy → b ← (view ˢᵛb̲uttons 2) ˢᵛh̲it xy b = 1 ? ˡn̲ew 0 b = 2 ? ˡh̲int s k ← (ˡs̲quares @) ˢᵛh̲it xy k > 0 ? s ˡp̲ress k s }
ˡs̲ay : Num a => a -> Char
What to say: how many lights are on and how many moves, or the win.
ˡs̲ay ← { s → 1 = ˡs̲tatus s ? @ f̲ormat< "All the lights are out, in {h:c_ount l:m_oves s}!" (2 s̲elect s) > 0 ? "Hint: press the square ringed in blue." @ f̲ormat< "{'+ r_/ l:l_ights s} lights on, {h:c_ount l:m_oves s}." }
ʰc̲ount : Num a => a -> Char
A count of moves in words: 1 move, 2 moves.
ʰc̲ount ← { n → 1 = n ? "1 move"◆ @ f̲ormat< "{n} moves" }
ʰf̲ill : Int -> Char
A light's fill: on yellow, off dark.
ʰf̲ill ← { on → d̲isclose (1 + on) s̲elect "#2b2d42" "#ffd84d" }
ˡd̲raw : Int -> Char
The picture, as SVG text: the buttons, the 25 lights (the hint's square ringed), and what to say. Every coordinate is computed here.
ˡd̲raw ← { s → q ← ˡs̲quares @ b ← ˡl̲ights s bg ← view ˢᵛr̲ect "fill=\"#14213d\"" lights ← '{ k → (k s̲elect q) ˢᵛr̲ect @ f̲ormat< "class=\"light\" rx=\"18\" fill=\"{h:f_ill k s_elect b}\" stroke=\"#000000\" stroke-width=\"4\"" } e̲ach r̲ange 25 h ← 2 s̲elect s ring ← (h > 0) r̲eplicate '{ k → (((k s̲elect q) + -12 -12 24 24.0)) ˢᵛr̲ect "fill=\"none\" stroke=\"#4cc9f0\" stroke-width=\"14\" rx=\"26\"" } e̲ach 1 m̲ax h words ← (1250 450 44.0) ˢᵛw̲ords ˡs̲ay s view ˢᵛp̲icture bg c̲at ((view c̲at 0.0) ˢᵛl̲abeled "New puzzle" "Hint") c̲at lights c̲at ring c̲at words }