librarygames/stargazer/Sky.xtl
Stargazer's rules and its sky, a library: stargazer.xtl (scripted), play.xtl (the game: a sky you click) and the web page all use these. The data is in two TOML files, read with []L_IST and []T_ABLE: assets/cache/sky.toml (the Bright Star Catalog's stars and the IAU's named stars, fetched by assets/fetch.sh, not tracked) and stars.toml (the parts of the sky, the brightness bands, stars of your own). The sky is SVG written here, with the pieces of lib/Svg.xtl.
Sky units are hundredths of a degree: x = 100 (360 - ra), so right ascension grows to the left as on the sky seen from Earth, and y = 100 (90 - dec) south from the north pole. A game's state: the part of the sky (0 all of it), the star whose menu is open (0 none), the score, the number of stars n, the round's random number, then the n stars, the n results (0 not yet, 1 right, 2 wrong), then the last star answered and the star chosen for it.
The data
named : Box Char
The named stars, then any of your own: name, ra, dec, magnitude and constellation, as texts.
named ← (("assets/cache/sky.toml" "star") ⎕T̲ABLE ("stars" "fields")) c̲at ("stars.toml" "star") ⎕T̲ABLE ("stars" "fields")
ʰs̲econd : Box Char -> Float
The second and third numbers in a text (h:, private to this file).
ʰs̲econd ← { x → 2 s̲elect n̲umbers d̲isclose x }
parts : Box Char
The parts of the sky: name, ra0, ra1, dec0, dec1.
parts ← ("stars.toml" "area") ⎕T̲ABLE ("areas" "areafields")
edges : Float
The brightness bands' upper edges, each star's band (1 the brightest), and how many of each band a round takes.
edges ← 'ʰs̲econd e̲ach "stars.toml" ⎕L̲IST "bands"
sky : Box Char
The sky behind: every catalog star to magnitude 5.5, as paths of points (one per brightness class, a round dot of a fixed size on the screen at every zoom), joined once.
sky ← "assets/cache/sky.toml" ⎕L̲IST "sky"
class : Int
class ← 1 + '+ r̲/₂ 0 + ('ʰt̲hird e̲ach sky) '> t̲able 1.0 2.0 3.0 4.0
ʰp̲oints : Int -> Char
ʰp̲oints ← { k → ᵗj̲oin '{ j → e̲nclose "M" c̲at (ˢᵛw̲hole j s̲elect sx) c̲at " " c̲at (ˢᵛw̲hole j s̲elect sy) c̲at "h0" } e̲ach w̲here class = k }
ʰw̲idth : Int -> Char
ʰw̲idth ← { k → d̲isclose k s̲elect "7" "5.5" "4" "2.8" "1.8" }
layer : Char
layer ← ᵗj̲oin '{ k → e̲nclose @ f̲ormat< "<path d=\"{h:p_oints k}\" stroke-width=\"{h:w_idth k}\" vector-effect=\"non-scaling-stroke\"/>" } e̲ach r̲ange 5
f̲ormat< expands to
("<path d=\"" c̲at (f̲ormat (ʰp̲oints k)) c̲at "\" stroke-width=\"" c̲at (f̲ormat (ʰw̲idth k)) c̲at "\" vector-effect=\"non-scaling-stroke\"/>")
Where things are
ˡs̲x : Num a => a -> a
Sky x of right ascensions, and sky y of declinations (sky units).
ˡs̲x ← { r → 100 × 360 − r }
ˡs̲y : Num a => a -> a
Sky y of declinations (sky units): hundredths of a degree south of the north pole.
ˡs̲y ← { d → 100 × 90 − d }
ʰi̲nRa : Truthy a => Int -> a
Which named stars lie in part a's right ascensions (through 0h when ra0 is above ra1).
ʰi̲nRa ← { a → (a s̲elect ra0) ≤ a s̲elect ra1 ? (ra ≥ a s̲elect ra0) ∧ ra < a s̲elect ra1 (ra ≥ a s̲elect ra0) ∨ ra < a s̲elect ra1 }
ˡp̲ool : Int -> Int
The named stars in part a of the sky (0 all of it), by number.
ˡp̲ool ← { a → 0 = a ? r̲ange t̲ally names w̲here (ʰi̲nRa a) ∧ (dec ≥ a s̲elect dec0) ∧ dec ≤ a s̲elect dec1 }
ˡp̲arts : Unit -> Box Char
The parts of the sky, by name, in the menu's order.
ˡp̲arts ← { @ → 1 s̲elect₂ parts }
ˡv̲iew : Int -> Float
What the sky shows for part a: x, y, width and height (sky units), in the shape of a picture (5 by 2) with room at the top for the buttons; the whole sky (a 0) a little wider than one turn, drawn again on both sides.
ˡv̲iew ← { a → 0 = a ? -4500.0 -1000.0 45000.0 18000.0 r1 ← (a s̲elect ra1) + 360 × f̲loat (a s̲elect ra0) > a s̲elect ra1 w ← 100 × r1 − a s̲elect ra0 h ← 100 × (a s̲elect dec1) − a s̲elect dec0 cx ← (ˡs̲x r1) + w ÷ 2 cy ← (ˡs̲y a s̲elect dec1) + h ÷ 2 w ← w m̲ax 2.5 × h h ← w ÷ 2.5 (cx − 0.6 × w) c̲at (cy − 0.7 × h) c̲at (1.2 × w) c̲at 1.2 × h }
ˡa̲t : Int -> Int -> Float
Star i's point in part a's view: x and y, x moved by a whole turn when that brings it nearer the middle of the view (the sky wraps at 0h).
ˡa̲t ← { a i → v ← ˡv̲iew a x ← ˡs̲x i s̲elect ra mid ← (f̲irst v) + 0.5 × 3 s̲elect v (x − 36000.0 × f̲loat f̲loor 0.5 + (x − mid) ÷ 36000) c̲at ˡs̲y i s̲elect dec }
ˡc̲hoices : Int -> Int -> Int
Star i's choices, with the round's random number q: it and one star from each brightness band (another star, from anywhere in the sky, picked by q and i), in the order of right ascension.
ˡc̲hoices ← { q i → c ← i c̲at '{ b → q ʰo̲ne i c̲at b } e̲ach r̲ange t̲ally mix (g̲rade c s̲elect n̲eg ra) s̲elect c }
ʰo̲ne : Int -> Int -> Int
Band b's star for star i: a star of that band other than i, at a place fixed by q, i and b.
ʰo̲ne ← { q ib → l ← w̲here band = 2 s̲elect ib l ← (l ≠ f̲irst ib) r̲eplicate l (1 + (q + (7919 × f̲irst ib) + 104729 × 2 s̲elect ib) m̲od t̲ally l) s̲elect l }
The game
ʰu̲pTo : Int -> a -> a
The first n items of v, or all of them when v has fewer (a band with no star in a part of the sky: the round is filled from the rest).
ʰu̲pTo ← { n v → (n m̲in t̲ally v) t̲ake v }
ˡn̲ew : Int -> Int
A new round in part a of the sky (0 all of it): its named stars in a random order, mix of each band taken in turn (bright, middling, faint), filled up from the rest when a band has too few; five, or all of them when the part has fewer.
ˡn̲ew ← { a → p ← ˡp̲ool a o ← (g̲rade r̲oll! (t̲ally p) r̲eshape 1000000) s̲elect p b ← o s̲elect band pick ← d̲isclose '{ x y → e̲nclose (d̲isclose x) c̲at d̲isclose y } r̲/ '{ k → (k s̲elect mix) ʰu̲pTo (b = k) r̲eplicate o } m̲ap r̲ange t̲ally mix n ← 5 m̲in t̲ally p stars ← n t̲ake pick c̲at (n̲ot o m̲ember? pick) r̲eplicate o a c̲at 0 0 c̲at n c̲at (r̲oll! 1000000) c̲at stars c̲at (n r̲eshape 0) c̲at 0 0 }
ˡm̲arks : Int -> Int
The round's stars, and each one's result (0 not yet, 1 right, 2 wrong).
ˡm̲arks ← { s → (4 s̲elect s) t̲ake 5 d̲rop s }
ˡr̲esults : Int -> Int
Each star's result in the round: 0 not yet, 1 right, 2 wrong.
ˡr̲esults ← { s → (4 s̲elect s) t̲ake (5 + 4 s̲elect s) d̲rop s }
ˡs̲tatus : (Num a, Truthy a) => Int -> a
0 playing, 1 when every star is answered.
ˡs̲tatus ← { s → 0 + 0 = '+ r̲/ 0 + 0 = ˡr̲esults s }
ˡo̲ffered : Int -> Int
The open star's choices.
ˡo̲ffered ← { s → (5 s̲elect s) ˡc̲hoices (2 s̲elect s) s̲elect ˡm̲arks s }
ˡm̲enu : Int -> Float
The open star's menu: four rectangles beside it (Svg's menu).
ˡm̲enu ← { s → (ˡv̲iew f̲irst s) ˢᵛm̲enu (f̲irst s) ˡa̲t (2 s̲elect s) s̲elect ˡm̲arks s }
ʰx̲s : Int -> Float
The marked stars' x and y in their view.
ʰx̲s ← { s → '{ i → f̲irst (f̲irst s) ˡa̲t i } e̲ach ˡm̲arks s }
ʰs̲tarAt : Int -> Float -> Int
Which unanswered star is at xy: the nearest within reach, 1 to n, 0 for none.
ʰs̲tarAt ← { s xy → u ← ˢᵛu̲nit ˡv̲iew f̲irst s dx ← (ʰx̲s s) − f̲irst xy dy ← (ʰy̲s s) − 2 s̲elect xy d2 ← (dx × dx) + dy × dy ok ← (d2 ≤ (u × 16) × u × 16) ∧ 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 (sky units): a part's button starts a round there; in an open menu a choice answers its star; a star 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 ˡp̲arts @) ˢᵛh̲it xy b > 0 ? ˡn̲ew b − 1 (2 s̲elect s) > 0 ? s ʰo̲pen xy (2 c̲at s ʰs̲tarAt xy) ʰs̲et s }
ʰo̲pen : Int -> Float -> Int
A click while a menu is open: a choice answers; elsewhere, another star'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 ʰs̲tarAt 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 star: right or wrong, the score, the menu closed.
ʰa̲nswer ← { s k → d ← 2 s̲elect s i ← d s̲elect ˡm̲arks s pick ← k s̲elect ˡo̲ffered s right ← pick = i n ← 4 s̲elect s t ← (5 + 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 part of the sky 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 part of the sky 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 star 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 ringed star: which is it?" i ← d s̲elect ˡm̲arks s 1 = last ? @ f̲ormat< "Right: {l:n_ame i}, in {d_isclose i s_elect 5 s_elect_2 named}.{note}" @ f̲ormat< "No: that is {l:n_ame i}, not {l:n_ame (t_ally s) s_elect s}.{note}" }
f̲ormat< expands to
("Right: " c̲at (f̲ormat (ˡn̲ame i)) c̲at ", in " c̲at (f̲ormat (d̲isclose i s̲elect 5 s̲elect₂ named)) c̲at "." c̲at (f̲ormat (note)))
ʰr̲ingColor : Int -> Char
A ring's color by its result (0 not yet, red; 1 right, green; 2 wrong, orange).
ʰr̲ingColor ← { r → d̲isclose (1 + r) s̲elect "#ff4d4d" "#3ddc84" "#ffa630" }
ʰr̲ings : Int -> Float -> Box Char
The round's stars: a ring around each (its color its result), each answered one named.
ʰr̲ings ← { s u → x ← ʰx̲s s y ← ʰy̲s s r ← ˡr̲esults s rings ← '{ 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 * 13}\" fill=\"none\" stroke=\"{h:r_ingColor k s_elect r}\" stroke-width=\"{sv:w_hole u * 3}\"/>" } e̲ach r̲ange t̲ally x rings c̲at '{ k → (((k s̲elect x) + u × 16) c̲at ((k s̲elect y) + u × 6) c̲at u × 18) ˢᵛw̲ords ˡn̲ame k s̲elect ˡm̲arks s } e̲ach w̲here r > 0 }
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 × 13)) c̲at "\" fill=\"none\" stroke=\"" c̲at (f̲ormat (ʰr̲ingColor k s̲elect r)) c̲at "\" stroke-width=\"" c̲at (f̲ormat (ˢᵛw̲hole u × 3)) c̲at "\"/>")
ʰb̲ar : Num a => a -> Float -> Box Char
The parts' buttons (the part shown filled), and the score below them at the right (seven buttons fill the row).
ʰ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 × 80) c̲at u × 22 ((v c̲at f̲loat 1 + f̲irst s) ˢᵛl̲abeled (e̲nclose "All") c̲at ˡp̲arts @) 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 names, each a button.
ʰc̲hoose ← { s → 0 = 2 s̲elect s ? 0 r̲eshape e̲nclose "" (ˡm̲enu s) ˢᵛc̲hoices '{ i → e̲nclose ˡn̲ame i } e̲ach ˡo̲ffered s }
ˡd̲raw : Int -> Char
The picture, as SVG text: the night, the sky's stars (drawn three times, a turn apart, so a view across 0h is whole), the round's rings, the parts' 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 night ← v ˢᵛr̲ect "fill=\"#0b1d3a\"" stars ← e̲nclose "<g id=\"sky\" stroke=\"#ffffff\" stroke-linecap=\"round\" fill=\"none\">" c̲at layer c̲at "</g><use href=\"#sky\" x=\"-36000\"/><use href=\"#sky\" x=\"36000\"/>" v ˢᵛp̲icture night c̲at stars c̲at (s ʰr̲ings u) c̲at (s ʰb̲ar v) c̲at (s ʰs̲aid v) c̲at ʰc̲hoose s }