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}!"
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}."
120}
121⍝# A count of moves in words: 1 move, 2 moves.
122ʰc̲ount ← { n → 1 = n ? "1 move"◆ @ f̲ormat< "{n} 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
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}
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 "."
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\"")