sourcegames/capitals/Atlas.xtl
1⍝# The capitals game's rules and its map, a library: capitals.xtl
2⍝# (scripted), play.xtl (the game: a map you click) and the web page all
3⍝# use these. (Named Atlas because a disk that ignores case cannot hold
4⍝# Capitals.xtl beside capitals.xtl.) The data is in two TOML files, read
5⍝# with []L_IST and []T_ABLE: assets/cache/world.toml (Natural Earth's
6⍝# country outlines, national capitals and cities, fetched by
7⍝# assets/fetch.sh, not tracked) and places.toml (the regions, and
8⍝# places of your own). The map is SVG written here.
9⍝#
10⍝# Map units are hundredths of a degree: x = 100 (lon + 180) east from
11⍝# 180 W, y = 100 (90 - lat) south from the pole (the plate carree). A
12⍝# game's state: the region (0 the whole world), the dot whose menu is
13⍝# open (0 none), the score, the number of dots n, then the n places,
14⍝# the n results (0 not yet, 1 right, 2 wrong), then the last dot
15⍝# answered and the city chosen for it.
16
17ᵗ⁼u̲se< "Text"
18ˢᵛ⁼u̲se< "Svg"
19
20⍝## The data
21
22⍝# Every place: the capitals, then places of your own; a row each of
23⍝# name, latitude, longitude and region, as texts.
24all ← (("assets/cache/world.toml" "place") ⎕T̲ABLE ("places" "fields")) c̲at ("places.toml" "place") ⎕T̲ABLE ("places" "fields")
25⍝# The cities the wrong choices come from: Natural Earth's populated
26⍝# places, then places of your own; name, latitude, longitude.
27towns ← (("assets/cache/world.toml" "city") ⎕T̲ABLE ("cities" "cityfields")) c̲at 1 2 3 s̲elect₂ ("places.toml" "place") ⎕T̲ABLE ("places" "fields")
28⍝# A name up to its comma: "Paris, France" is Paris (h:, private to this file).
29ʰc̲ity ← { x →
30 t ← d̲isclose x
31 (n̲ot '∨ s̲\ t = f̲irst ",") r̲eplicate t
32}
33names ← 1 s̲elect₂ all
34lat ← 'ˢᵛn̲um e̲ach 2 s̲elect₂ all
35lon ← 'ˢᵛn̲um e̲ach 3 s̲elect₂ all
36regions ← "places.toml" ⎕L̲IST "regions"
37region ← regions i̲ndexOf 4 s̲elect₂ all
38tlat ← 'ˢᵛn̲um e̲ach 2 s̲elect₂ towns
39tlon ← 'ˢᵛn̲um e̲ach 3 s̲elect₂ towns
40⍝# The countries, unlabeled: each outline (SVG path data in degrees)
41⍝# wrapped as a path, joined once.
42land ← ᵗj̲oin '{ d → e̲nclose "<path d=\"" c̲at (d̲isclose d) c̲at "\"/>" } e̲ach "assets/cache/world.toml" ⎕L̲IST "countries"
43
44⍝## Where things are
45
46⍝ :: Num a => a -> a
47⍝# Map x of longitudes, and map y of latitudes (map units).
48ˡm̲x ← { lo → 100 × 180 + lo }
49⍝ :: Num a => a -> a
50⍝# Map y of latitudes (map units): hundredths of a degree south of the north pole.
51ˡm̲y ← { la → 100 × 90 − la }
52⍝ :: Int -> Int
53⍝# The places of region r (0 the whole world), by number.
54ˡp̲ool ← { r → w̲here (r = 0) ∨ region = r }
55⍝ :: Unit -> Box Char
56⍝# The regions, in the menu's order (from places.toml).
57ˡr̲egions ← { @ → regions }
58⍝ :: Int -> Float
59⍝# Place i's point on the map: x and y (map units).
60ˡa̲t ← { i → (ˡm̲x i s̲elect lon) c̲at ˡm̲y i s̲elect lat }
61⍝ :: Int -> Char
62⍝# Place i's name ("Paris, France").
63ˡn̲ame ← { i → d̲isclose i s̲elect names }
64⍝ :: Int -> Float
65⍝# What the map shows for region r: x, y, width and height (map units),
66⍝# the region's places with a margin, at least 30 degrees wide and
67⍝# widened or heightened to the shape of the whole map (5 by 2), with
68⍝# room at the top for the buttons.
69ˡv̲iew ← { r →
70 0 = r ? 0.0 600.0 36000.0 14400.0
71 p ← ˡp̲ool r
72 x ← ˡm̲x p s̲elect lon
73 y ← ˡm̲y p s̲elect lat
74 w ← 3000.0 m̲ax 1.3 × ('m̲ax r̲/ x) − 'm̲in r̲/ x
75 h ← 1.3 × ('m̲ax r̲/ y) − 'm̲in r̲/ y
76 w ← w m̲ax 2.5 × h
77 h ← w ÷ 2.5
78 cx ← ('m̲ax r̲/ x) + 'm̲in r̲/ x
79 cy ← ('m̲ax r̲/ y) + 'm̲in r̲/ y
80 ((cx − 1.2 × w) ÷ 2) c̲at (((cy − h) ÷ 2) − 0.2 × h) c̲at (1.2 × w) c̲at 1.2 × h
81}
82⍝ :: Int -> Float
83⍝# The cosine of the angle at the Earth's center between place i and
84⍝# every city at once (the spherical law of cosines): larger is nearer.
85ˡc̲lose ← { i →
86 r ← (p̲i @) ÷ 180
87 a ← r × i s̲elect lat
88 b ← r × i s̲elect lon
89 ((s̲in a) × s̲in r × tlat) + (c̲os a) × (c̲os r × tlat) × c̲os b − r × tlon
90}
91⍝ :: Int -> Char
92⍝# City k's name, up to any comma.
93ˡt̲own ← { k → ʰc̲ity k s̲elect 1 s̲elect₂ towns }
94⍝ :: Int -> Int
95⍝# Place i's choices, as city numbers: the nearest cities with
96⍝# different names, four of them, west to east. The nearest is the
97⍝# place itself (every capital is among the cities), so it is the answer.
98ˡc̲hoices ← { i →
99 o ← 12 t̲ake g̲rade n̲eg ˡc̲lose i
100 nm ← '{ k → e̲nclose ˡt̲own k } e̲ach o
101 c ← 4 t̲ake ((nm i̲ndexOf nm) = r̲ange t̲ally nm) r̲eplicate o
102 (g̲rade c s̲elect tlon) s̲elect c
103}
104⍝ :: Int -> Int
105⍝# Place i's answer among the cities.
106ˡa̲nswer ← { i → f̲irst g̲rade n̲eg ˡc̲lose i }
107
108⍝## The game
109
110⍝ :: Int -> Int
111⍝# A new round in region r (0 the whole world): five of its places drawn
112⍝# at random (all of them when it has fewer), no menu open, no score.
113ˡn̲ew ← { r →
114 p ← ˡp̲ool r
115 n ← 5 m̲in t̲ally p
116 r c̲at 0 0 c̲at n c̲at (n t̲ake (g̲rade r̲oll! (t̲ally p) r̲eshape 1000000) s̲elect p) c̲at (n r̲eshape 0) c̲at 0 0
117}
118⍝ :: a -> a
119⍝# The round's score.
120ˡs̲core ← { s → 3 s̲elect s }
121⍝ :: Int -> Int
122⍝# The round's places, and each one's result (0 not yet, 1 right, 2 wrong).
123ˡd̲ots ← { s → (4 s̲elect s) t̲ake 4 d̲rop s }
124⍝ :: Int -> Int
125⍝# Each dot's result in the round: 0 not yet, 1 right, 2 wrong.
126ˡr̲esults ← { s → (4 s̲elect s) t̲ake (4 + 4 s̲elect s) d̲rop s }
127⍝ :: (Num a, Truthy a) => Int -> a
128⍝# 0 playing, 1 when every dot is answered.
129ˡs̲tatus ← { s → 0 + 0 = '+ r̲/ 0 + 0 = ˡr̲esults s }
130
131⍝ :: Int -> Float
132⍝# The open dot's menu: a row each of x, y, width and height for its
133⍝# four choices, beside the dot (Svg's menu), kept inside the view.
134ˡm̲enu ← { s → (ˡv̲iew f̲irst s) ˢᵛm̲enu ˡa̲t (2 s̲elect s) s̲elect ˡd̲ots s }
135⍝# Which unanswered dot is at xy: the nearest within reach (dots of close
136⍝# capitals overlap), 1 to n, 0 for none.
137ʰd̲ot ← { s xy →
138 u ← ˢᵛu̲nit ˡv̲iew f̲irst s
139 p ← ˡd̲ots s
140 dx ← (ˡm̲x p s̲elect lon) − f̲irst xy
141 dy ← (ˡm̲y p s̲elect lat) − 2 s̲elect xy
142 d2 ← (dx × dx) + dy × dy
143 ok ← (d2 ≤ (u × 14) × u × 14) ∧ 0 = ˡr̲esults s
144 0 = '+ r̲/ 0 + ok ? 0
145 f̲irst g̲rade d2 + 1e12 × f̲loat n̲ot ok
146}
147⍝ :: Int -> Float -> Int
148⍝# A click at xy (map units): a region button starts a round there; in
149⍝# an open menu a choice answers its dot; a dot not yet answered opens
150⍝# its menu; anywhere else closes the menu.
151ˡc̲lick ← { s xy →
152 b ← ((ˡv̲iew f̲irst s) ˢᵛb̲uttons 1 + t̲ally regions) ˢᵛh̲it xy
153 b > 0 ? ˡn̲ew b − 1
154 (2 s̲elect s) > 0 ? s ʰo̲pen xy
155 (2 c̲at s ʰd̲ot xy) ʰs̲et s
156}
157⍝# A click while a menu is open: a choice answers; elsewhere, another
158⍝# dot's menu opens or the menu closes.
159ʰo̲pen ← { s xy →
160 k ← (ˡm̲enu s) ˢᵛh̲it xy
161 k > 0 ? s ʰa̲nswer k
162 (2 c̲at s ʰd̲ot xy) ʰs̲et s
163}
164⍝# The state with item k (from 1) replaced by v: kv is k v.
165ʰs̲et ← { kv s → s + ((2 s̲elect kv) − (f̲irst kv) s̲elect s) × (f̲irst kv) = r̲ange t̲ally s }
166⍝# Choice k of the open dot: right or wrong, the score, the menu closed.
167ʰa̲nswer ← { s k →
168 d ← 2 s̲elect s
169 i ← d s̲elect ˡd̲ots s
170 pick ← k s̲elect ˡc̲hoices i
171 right ← pick = ˡa̲nswer i
172 n ← 4 s̲elect s
173 t ← (4 + n + d) c̲at 2 − right
174 s ← (2 c̲at 0) ʰs̲et (3 c̲at (3 s̲elect s) + right) ʰs̲et t ʰs̲et s
175 (-2 d̲rop s) c̲at d c̲at pick
176}
177
178⍝## What the game says and draws
179
180⍝# The end of a round, said after its last answer (else nothing).
181ʰr̲oundOver ← { s →
182 0 = ˡs̲tatus s ? ""
183 @ f̲ormat< " Round over: {l:s_core s} of {4 s_elect s}. Choose a region for another round."
184}
185⍝ :: Int -> Char
186⍝# What has happened, in a line: the open menu, the last answer, the
187⍝# round's end.
188ˡs̲ay ← { s →
189 (2 s̲elect s) > 0 ? "Which city is this?"
190 d ← ((t̲ally s) − 1) s̲elect s
191 last ← (1 + d) s̲elect 0 c̲at ˡr̲esults s
192 note ← ʰr̲oundOver s
193 0 = d ? "Click a red dot: which city is it?"
194 i ← d s̲elect ˡd̲ots s
195 1 = last ? @ f̲ormat< "Right: {l:n_ame i}.{note}"
196 @ f̲ormat< "No: that is {l:n_ame i}, not {l:t_own (t_ally s) s_elect s}.{note}"
197}
198⍝# A dot's color by its result (0 not yet, red; 1 right, green; 2
199⍝# wrong, orange).
200ʰd̲otColor ← { r → d̲isclose (1 + r) s̲elect "#d62828" "#2a9d3a" "#f08c00" }
201⍝# The round's dots: red not yet answered, green right, orange wrong;
202⍝# each answered one named.
203ʰs̲pots ← { s u →
204 p ← ˡd̲ots s
205 r ← ˡr̲esults s
206 x ← ˡm̲x p s̲elect lon
207 y ← ˡm̲y p s̲elect lat
208 dots ← '{ k → e̲nclose @ f̲ormat< "<circle class=\"dot\" cx=\"{sv:w_hole k s_elect x}\" cy=\"{sv:w_hole k s_elect y}\" r=\"{sv:w_hole u * 9}\" fill=\"{h:d_otColor k s_elect r}\" stroke=\"#ffffff\" stroke-width=\"{sv:w_hole u * 2}\"/>" } e̲ach r̲ange t̲ally p
209 named ← w̲here r > 0
210 dots c̲at '{ k → (((k s̲elect x) + u × 13) c̲at ((k s̲elect y) + u × 6) c̲at u × 18) ˢᵛw̲ords ʰc̲ity (k s̲elect p) s̲elect names } e̲ach named
211}
212⍝# The region buttons (the region shown filled), and the score.
213ʰb̲ar ← { s v →
214 u ← ˢᵛu̲nit v
215 score ← ((f̲irst v) + (3 s̲elect v) − u × 190) c̲at ((2 s̲elect v) + u × 34) c̲at u × 22
216 ((v c̲at f̲loat 1 + f̲irst s) ˢᵛl̲abeled (e̲nclose "World") c̲at regions) c̲at score ˢᵛw̲ords @ f̲ormat< "Score {l:s_core s} of {4 s_elect s}"
217}
218⍝# What happened, along the bottom.
219ʰs̲aid ← { s v →
220 u ← ˢᵛu̲nit v
221 (((f̲irst v) + u × 12) c̲at ((2 s̲elect v) + (4 s̲elect v) − u × 16) c̲at u × 22) ˢᵛw̲ords ˡs̲ay s
222}
223⍝# The open menu: the four choices, each a button.
224ʰc̲hoose ← { s →
225 0 = 2 s̲elect s ? 0 r̲eshape e̲nclose ""
226 (ˡm̲enu s) ˢᵛc̲hoices '{ k → e̲nclose ˡt̲own k } e̲ach ˡc̲hoices (2 s̲elect s) s̲elect ˡd̲ots s
227}
228⍝ :: Int -> Char
229⍝# The picture, as SVG text: the sea, the countries (unlabeled, outlines
230⍝# in degrees under the projection's transform), the dots, the region
231⍝# buttons and the score, what happened, and the open menu. Every
232⍝# coordinate is computed here; the page only shows the picture.
233ˡd̲raw ← { s →
234 v ← ˡv̲iew f̲irst s
235 u ← ˢᵛu̲nit v
236 sea ← v ˢᵛr̲ect "fill=\"#bfdcef\""
237 open ← e̲nclose @ f̲ormat< "<g transform=\"matrix(100 0 0 -100 18000 9000)\" fill=\"#f3eee0\" stroke=\"#7a7a7a\" stroke-width=\"{u / 100}\">"
238 v ˢᵛp̲icture sea c̲at open c̲at (e̲nclose land) c̲at (e̲nclose "</g>") c̲at (s ʰs̲pots u) c̲at (s ʰb̲ar v) c̲at (s ʰs̲aid v) c̲at ʰc̲hoose s
239}
f̲ormat< expands to
" Round over: " c̲at (f̲ormat (ˡs̲core s)) c̲at " of " c̲at (f̲ormat (4 s̲elect s)) c̲at ". Choose a region for another round."
f̲ormat< expands to
"No: that is " c̲at (f̲ormat (ˡn̲ame i)) c̲at ", not " c̲at (f̲ormat (ˡt̲own (t̲ally s) s̲elect s)) c̲at "." c̲at (f̲ormat (note))
f̲ormat< expands to
("<circle class=\"dot\" cx=\"" c̲at (f̲ormat (ˢᵛw̲hole k s̲elect x)) c̲at "\" cy=\"" c̲at (f̲ormat (ˢᵛw̲hole k s̲elect y)) c̲at "\" r=\"" c̲at (f̲ormat (ˢᵛw̲hole u × 9)) c̲at "\" fill=\"" c̲at (f̲ormat (ʰd̲otColor k s̲elect r)) c̲at "\" stroke=\"#ffffff\" stroke-width=\"" c̲at (f̲ormat (ˢᵛw̲hole u × 2)) c̲at "\"/>")
f̲ormat< expands to
("<g transform=\"matrix(100 0 0 -100 18000 9000)\" fill=\"#f3eee0\" stroke=\"#7a7a7a\" stroke-width=\"" c̲at (f̲ormat (u ÷ 100)) c̲at "\">")