librarygames/capitals/Atlas.xtl
The capitals game's rules and its map, a library: capitals.xtl (scripted), play.xtl (the game: a map you click) and the web page all use these. (Named Atlas because a disk that ignores case cannot hold Capitals.xtl beside capitals.xtl.) The data is in two TOML files, read with []L_IST and []T_ABLE: assets/cache/world.toml (Natural Earth's country outlines, national capitals and cities, fetched by assets/fetch.sh, not tracked) and places.toml (the regions, and places of your own). The map is SVG written here.
Map units are hundredths of a degree: x = 100 (lon + 180) east from 180 W, y = 100 (90 - lat) south from the pole (the plate carree). A game's state: the region (0 the whole world), the dot whose menu is open (0 none), the score, the number of dots n, then the n places, the n results (0 not yet, 1 right, 2 wrong), then the last dot answered and the city chosen for it.
The data
all : Box Char
Every place: the capitals, then places of your own; a row each of name, latitude, longitude and region, as texts.
all ← (("assets/cache/world.toml" "place") ⎕T̲ABLE ("places" "fields")) c̲at ("places.toml" "place") ⎕T̲ABLE ("places" "fields")
towns : Box Char
The cities the wrong choices come from: Natural Earth's populated places, then places of your own; name, latitude, longitude.
towns ← (("assets/cache/world.toml" "city") ⎕T̲ABLE ("cities" "cityfields")) c̲at 1 2 3 s̲elect₂ ("places.toml" "place") ⎕T̲ABLE ("places" "fields")
ʰc̲ity : Box Char -> Char
A name up to its comma: "Paris, France" is Paris (h:, private to this file).
ʰc̲ity ← { x → t ← d̲isclose x (n̲ot '∨ s̲\ t = f̲irst ",") r̲eplicate t }
land : Char
The countries, unlabeled: each outline (SVG path data in degrees) wrapped as a path, joined once.
land ← ᵗj̲oin '{ d → e̲nclose "<path d=\"" c̲at (d̲isclose d) c̲at "\"/>" } e̲ach "assets/cache/world.toml" ⎕L̲IST "countries"
Where things are
ˡm̲x : Num a => a -> a
Map x of longitudes, and map y of latitudes (map units).
ˡm̲x ← { lo → 100 × 180 + lo }
ˡm̲y : Num a => a -> a
Map y of latitudes (map units): hundredths of a degree south of the north pole.
ˡm̲y ← { la → 100 × 90 − la }
ˡp̲ool : Int -> Int
The places of region r (0 the whole world), by number.
ˡp̲ool ← { r → w̲here (r = 0) ∨ region = r }
ˡr̲egions : Unit -> Box Char
The regions, in the menu's order (from places.toml).
ˡr̲egions ← { @ → regions }
ˡa̲t : Int -> Float
Place i's point on the map: x and y (map units).
ˡa̲t ← { i → (ˡm̲x i s̲elect lon) c̲at ˡm̲y i s̲elect lat }
ˡv̲iew : Int -> Float
What the map shows for region r: x, y, width and height (map units), the region's places with a margin, at least 30 degrees wide and widened or heightened to the shape of the whole map (5 by 2), with room at the top for the buttons.
ˡv̲iew ← { r → 0 = r ? 0.0 600.0 36000.0 14400.0 p ← ˡp̲ool r x ← ˡm̲x p s̲elect lon y ← ˡm̲y p s̲elect lat w ← 3000.0 m̲ax 1.3 × ('m̲ax r̲/ x) − 'm̲in r̲/ x h ← 1.3 × ('m̲ax r̲/ y) − 'm̲in r̲/ y w ← w m̲ax 2.5 × h h ← w ÷ 2.5 cx ← ('m̲ax r̲/ x) + 'm̲in r̲/ x cy ← ('m̲ax r̲/ y) + 'm̲in r̲/ y ((cx − 1.2 × w) ÷ 2) c̲at (((cy − h) ÷ 2) − 0.2 × h) c̲at (1.2 × w) c̲at 1.2 × h }
ˡc̲lose : Int -> Float
The cosine of the angle at the Earth's center between place i and every city at once (the spherical law of cosines): larger is nearer.
ˡc̲lose ← { i → r ← (p̲i @) ÷ 180 a ← r × i s̲elect lat b ← r × i s̲elect lon ((s̲in a) × s̲in r × tlat) + (c̲os a) × (c̲os r × tlat) × c̲os b − r × tlon }
ˡt̲own : Int -> Char
City k's name, up to any comma.
ˡt̲own ← { k → ʰc̲ity k s̲elect 1 s̲elect₂ towns }
ˡc̲hoices : Int -> Int
Place i's choices, as city numbers: the nearest cities with different names, four of them, west to east. The nearest is the place itself (every capital is among the cities), so it is the answer.
ˡc̲hoices ← { i → o ← 12 t̲ake g̲rade n̲eg ˡc̲lose i nm ← '{ k → e̲nclose ˡt̲own k } e̲ach o c ← 4 t̲ake ((nm i̲ndexOf nm) = r̲ange t̲ally nm) r̲eplicate o (g̲rade c s̲elect tlon) s̲elect c }
ˡa̲nswer : Int -> Int
Place i's answer among the cities.
ˡa̲nswer ← { i → f̲irst g̲rade n̲eg ˡc̲lose i }
The game
ˡn̲ew : Int -> Int
A new round in region r (0 the whole world): five of its places drawn at random (all of them when it has fewer), no menu open, no score.
ˡn̲ew ← { r → p ← ˡp̲ool r n ← 5 m̲in t̲ally p 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 }
ˡd̲ots : Int -> Int
The round's places, and each one's result (0 not yet, 1 right, 2 wrong).
ˡd̲ots ← { s → (4 s̲elect s) t̲ake 4 d̲rop s }
ˡr̲esults : Int -> Int
Each dot's result in the round: 0 not yet, 1 right, 2 wrong.
ˡr̲esults ← { s → (4 s̲elect s) t̲ake (4 + 4 s̲elect s) d̲rop s }
ˡs̲tatus : (Num a, Truthy a) => Int -> a
0 playing, 1 when every dot is answered.
ˡs̲tatus ← { s → 0 + 0 = '+ r̲/ 0 + 0 = ˡr̲esults s }
ˡm̲enu : Int -> Float
The open dot's menu: a row each of x, y, width and height for its four choices, beside the dot (Svg's menu), kept inside the view.
ˡm̲enu ← { s → (ˡv̲iew f̲irst s) ˢᵛm̲enu ˡa̲t (2 s̲elect s) s̲elect ˡd̲ots s }
ʰd̲ot : Int -> Float -> Int
Which unanswered dot is at xy: the nearest within reach (dots of close capitals overlap), 1 to n, 0 for none.
ʰd̲ot ← { s xy → u ← ˢᵛu̲nit ˡv̲iew f̲irst s p ← ˡd̲ots s dx ← (ˡm̲x p s̲elect lon) − f̲irst xy dy ← (ˡm̲y p s̲elect lat) − 2 s̲elect xy d2 ← (dx × dx) + dy × dy ok ← (d2 ≤ (u × 14) × u × 14) ∧ 0 = ˡr̲esults s 0 = '+ r̲/ 0 + ok ? 0 f̲irst g̲rade d2 + 1e12 × f̲loat n̲ot ok }
ˡc̲lick : Int -> Float -> Int
A click at xy (map units): a region button starts a round there; in an open menu a choice answers its dot; a dot not yet answered opens its menu; anywhere else closes the menu.
ˡc̲lick ← { s xy → b ← ((ˡv̲iew f̲irst s) ˢᵛb̲uttons 1 + t̲ally regions) ˢᵛh̲it xy b > 0 ? ˡn̲ew b − 1 (2 s̲elect s) > 0 ? s ʰo̲pen xy (2 c̲at s ʰd̲ot xy) ʰs̲et s }
ʰo̲pen : Int -> Float -> Int
A click while a menu is open: a choice answers; elsewhere, another dot's menu opens or the menu closes.
ʰo̲pen ← { s xy → k ← (ˡm̲enu s) ˢᵛh̲it xy k > 0 ? s ʰa̲nswer k (2 c̲at s ʰd̲ot xy) ʰs̲et s }
ʰs̲et : Int -> Int -> Int
The state with item k (from 1) replaced by v: kv is k v.
ʰs̲et ← { kv s → s + ((2 s̲elect kv) − (f̲irst kv) s̲elect s) × (f̲irst kv) = r̲ange t̲ally s }
ʰa̲nswer : Int -> Int -> Int
Choice k of the open dot: right or wrong, the score, the menu closed.
ʰa̲nswer ← { s k → d ← 2 s̲elect s i ← d s̲elect ˡd̲ots s pick ← k s̲elect ˡc̲hoices i right ← pick = ˡa̲nswer i n ← 4 s̲elect s t ← (4 + n + d) c̲at 2 − right s ← (2 c̲at 0) ʰs̲et (3 c̲at (3 s̲elect s) + right) ʰs̲et t ʰs̲et s (-2 d̲rop s) c̲at d c̲at pick }
What the game says and draws
ʰr̲oundOver : Int -> Char
The end of a round, said after its last answer (else nothing).
ʰr̲oundOver ← { s → 0 = ˡs̲tatus s ? "" @ f̲ormat< " Round over: {l:s_core s} of {4 s_elect s}. Choose a region for another round." }
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."
ˡs̲ay : Int -> Char
What has happened, in a line: the open menu, the last answer, the round's end.
ˡs̲ay ← { s → (2 s̲elect s) > 0 ? "Which city is this?" d ← ((t̲ally s) − 1) s̲elect s last ← (1 + d) s̲elect 0 c̲at ˡr̲esults s note ← ʰr̲oundOver s 0 = d ? "Click a red dot: which city is it?" i ← d s̲elect ˡd̲ots s 1 = last ? @ f̲ormat< "Right: {l:n_ame i}.{note}" @ f̲ormat< "No: that is {l:n_ame i}, not {l:t_own (t_ally s) s_elect s}.{note}" }
ʰd̲otColor : Int -> Char
A dot's color by its result (0 not yet, red; 1 right, green; 2 wrong, orange).
ʰd̲otColor ← { r → d̲isclose (1 + r) s̲elect "#d62828" "#2a9d3a" "#f08c00" }
ʰs̲pots : Int -> Float -> Box Char
The round's dots: red not yet answered, green right, orange wrong; each answered one named.
ʰs̲pots ← { s u → p ← ˡd̲ots s r ← ˡr̲esults s x ← ˡm̲x p s̲elect lon y ← ˡm̲y p s̲elect lat 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 named ← w̲here r > 0 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 }
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 "\"/>")
ʰb̲ar : Num a => a -> Float -> Box Char
The region buttons (the region shown filled), and the score.
ʰb̲ar ← { s v → u ← ˢᵛu̲nit v score ← ((f̲irst v) + (3 s̲elect v) − u × 190) c̲at ((2 s̲elect v) + u × 34) c̲at u × 22 ((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}" }
ʰs̲aid : Int -> Float -> Box Char
What happened, along the bottom.
ʰs̲aid ← { s v → u ← ˢᵛu̲nit v (((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 }
ʰc̲hoose : Int -> Box Char
The open menu: the four choices, each a button.
ʰc̲hoose ← { s → 0 = 2 s̲elect s ? 0 r̲eshape e̲nclose "" (ˡm̲enu s) ˢᵛc̲hoices '{ k → e̲nclose ˡt̲own k } e̲ach ˡc̲hoices (2 s̲elect s) s̲elect ˡd̲ots s }
ˡd̲raw : Int -> Char
The picture, as SVG text: the sea, the countries (unlabeled, outlines in degrees under the projection's transform), the dots, the region buttons and the score, what happened, and the open menu. Every coordinate is computed here; the page only shows the picture.
ˡd̲raw ← { s → v ← ˡv̲iew f̲irst s u ← ˢᵛu̲nit v sea ← v ˢᵛr̲ect "fill=\"#bfdcef\"" open ← e̲nclose @ f̲ormat< "<g transform=\"matrix(100 0 0 -100 18000 9000)\" fill=\"#f3eee0\" stroke=\"#7a7a7a\" stroke-width=\"{u / 100}\">" 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 }