sourcegames/lights-out/Lamps.xtl

1⍝# Lights out's rules, a solver and the board's picture, a library: 2⍝# lights-out.xtl (scripted), play.xtl (the game: a board you click) and 3⍝# the web page all use these. The board is 25 lights, row by row, 1 on. 4⍝# A press toggles a light and its four neighbors: the press matrix, 5⍝# 25 by 25, says which, and a whole board of presses is one product 6⍝# with it, mod 2. A game's state: the moves made, the square a hint 7⍝# shows (0 none), then the 25 lights. 8 9ˢᵛ⁼u̲se< "Svg" 10 11⍝## The press matrix 12 13⍝ The row and column of each square, from 0. 14row ← ((r̲ange 25) − 1) d̲iv 5 15col ← ((r̲ange 25) − 1) m̲od 5 16⍝ :: (Num a, Truthy a) => Unit -> a 17⍝# Which lights each press toggles: row i is square i's press, a 1 on 18⍝# every square at most one step away (itself and its four neighbors), 19⍝# all 625 pairs at once by two tables of distances. 20⍝# >> "lo:" u_se< "Lamps" 21⍝# >> 5 5 r_eshape 13 s_elect lo:p_resses @ 22⍝# 0 0 0 0 0 23⍝# 0 0 1 0 0 24⍝# 0 1 1 1 0 25⍝# 0 0 1 0 0 26⍝# 0 0 0 0 0 27ˡp̲resses ← { @ → 0 + 1 ≥ (a̲bs row '− t̲able row) + a̲bs col '− t̲able col } 28⍝ :: Int -> Int -> Int 29⍝# The board after pressing the squares p (a 0/1 vector of 25) on board 30⍝# b: each light flips once for every press that reaches it, mod 2, one 31⍝# inner product for all of them. 32ˡa̲fter ← { b p → (b + p '+ '× i̲nner ˡp̲resses @) m̲od 2 } 33 34⍝## Solving it 35 36⍝# One column of Gaussian elimination mod 2 on the augmented matrix m 37⍝# (the press matrix, the board as its last column), rc the next pivot 38⍝# row and the column: a row with a 1 in the column is swapped up, and 39⍝# added (mod 2) to every other row with a 1 there; a column without one 40⍝# is a free press, left 0. 41ʰs̲tep ← { m rc → 42 r ← f̲irst rc 43 c ← 2 s̲elect rc 44 (c > 25) ∨ r > 25 ? m 45 cand ← w̲here (1 = c s̲elect₂ m) ∧ (r̲ange 25) ≥ r 46 0 = t̲ally cand ? m ʰs̲tep r c̲at c + 1 47 p ← f̲irst cand 48 k ← r̲ange 25 49 m ← (k + ((p − r) × k = r) + (r − p) × k = p) s̲elect m 50 hit ← 0 + (1 = c s̲elect₂ m) ∧ k ≠ r 51 m ← (m + hit '× t̲able r s̲elect m) m̲od 2 52 m ʰs̲tep (r + 1) c̲at c + 1 53} 54⍝ :: Int -> Int 55⍝# The presses that turn board b off (a 0/1 vector of 25), when there 56⍝# are any: the matrix reduced mod 2, each pivot's press read from the 57⍝# board's column; the free presses (5 by 5 has two) left 0. 58⍝# >> "lo:" u_se< "Lamps" 59⍝# >> b := 25 r_eshape 0 0 1 0 0 60⍝# >> 0 = '+ r_/ b lo:a_fter lo:s_olve b 61⍝# 1 62ˡs̲olve ← { b → 63 m ← ((ˡp̲resses @) c̲at₂ b) ʰs̲tep 1 1 64 lead ← '{ i → f̲irst (w̲here 1 = 25 t̲ake i s̲elect m) c̲at 0 } e̲ach r̲ange 25 65 last ← 26 s̲elect₂ m 66 '{ j → '+ r̲/ (lead = j) × last } e̲ach r̲ange 25 67} 68 69⍝## The game 70 71⍝ :: Any a => a -> Int 72⍝# A new puzzle: some squares pressed at random on a board all off, so 73⍝# it can always be solved (a board left all off gets its middle pressed). 74ˡn̲ew ← { x → 75 p ← 0 + 1 = r̲oll! 25 r̲eshape 2 76 p ← p + (0 = '+ r̲/ p) × 13 = r̲ange 25 77 0 0 c̲at (25 r̲eshape 0) ˡa̲fter p 78} 79⍝ :: a -> a 80⍝# The moves made. 81ˡm̲oves ← { s → f̲irst s } 82⍝ :: a -> a 83⍝# The 25 lights. 84ˡl̲ights ← { s → 2 d̲rop s } 85⍝ :: Int -> Int -> Int 86⍝# Square k (1 to 25) pressed: one more move, the hint gone. 87ˡp̲ress ← { s k → (1 + ˡm̲oves s) c̲at 0 c̲at (ˡl̲ights s) ˡa̲fter 0 + k = r̲ange 25 } 88⍝ :: Int -> Int 89⍝# A hint: the first square of a solution of the board as it is. 90ˡ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 } 91⍝ :: (Num a, Num b, Truthy b) => a -> b 92⍝# 0 playing, 1 every light off. 93ˡs̲tatus ← { s → 0 + 0 = '+ r̲/ ˡl̲ights s } 94 95⍝## The picture and the clicks 96 97⍝ The picture's view (2.5 by 1, the dialog's shape) and the board in it: 98⍝ 170 units a square, from (200, 130). 99view ← 0.0 0.0 2500.0 1000.0 100⍝ :: Unit -> Float 101⍝# The squares as rectangles (x, y, width, height), one row each, 1 to 25. 102ˡ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 } 103⍝ :: Int -> Float -> Int 104⍝# A click at xy: a square presses it; the first button starts a new 105⍝# puzzle, the second shows a hint. 106ˡc̲lick ← { s xy → 107 b ← (view ˢᵛb̲uttons 2) ˢᵛh̲it xy 108 b = 1 ? ˡn̲ew 0 109 b = 2 ? ˡh̲int s 110 k ← (ˡs̲quares @) ˢᵛh̲it xy 111 k > 0 ? s ˡp̲ress k 112 s 113} 114⍝ :: Num a => a -> Char 115⍝# What to say: how many lights are on and how many moves, or the win. 116ˡs̲ay ← { s → 117 1 = ˡs̲tatus s ? @ f̲ormat< "All the lights are out, in {h:c_ount l:m_oves s}!"
f̲ormat< expands to
("All the lights are out, in " c̲at (f̲ormat (ʰc̲ount ˡm̲oves s)) c̲at "!")
118 (2 s̲elect s) > 0 ? "Hint: press the square ringed in blue." 119 @ f̲ormat< "{'+ r_/ l:l_ights s} lights on, {h:c_ount l:m_oves s}."
f̲ormat< expands to
(f̲ormat ('+ r̲/ ˡl̲ights s)) c̲at " lights on, " c̲at (f̲ormat (ʰc̲ount ˡm̲oves s)) c̲at "."
120} 121⍝# A count of moves in words: 1 move, 2 moves. 122ʰc̲ount ← { n → 1 = n ? "1 move"◆ @ f̲ormat< "{n} moves" }
f̲ormat< expands to
(f̲ormat (n)) c̲at " moves"
123⍝# A light's fill: on yellow, off dark. 124ʰf̲ill ← { on → d̲isclose (1 + on) s̲elect "#2b2d42" "#ffd84d" } 125⍝ :: Int -> Char 126⍝# The picture, as SVG text: the buttons, the 25 lights (the hint's 127⍝# square ringed), and what to say. Every coordinate is computed here. 128ˡd̲raw ← { s → 129 q ← ˡs̲quares @ 130 b ← ˡl̲ights s 131 bg ← view ˢᵛr̲ect "fill=\"#14213d\"" 132 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
f̲ormat< expands to
("class=\"light\" rx=\"18\" fill=\"" c̲at (f̲ormat (ʰf̲ill k s̲elect b)) c̲at "\" stroke=\"#000000\" stroke-width=\"4\"")
133 h ← 2 s̲elect s 134 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 135 words ← (1250 450 44.0) ˢᵛw̲ords ˡs̲ay s 136 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 137}