A TADS3/adv3 module for computing the constellations visible in the night sky

This is a T3/adv3 module that implements a very simple ephemeris consisting of the IAU designated constellations plus the Pleiades: nightSky github repo.

The module works with the calendar module I put up the other day, using the local sidereal time to compute the constellations visible in the local sky, optionally computing the local altitude-azimuth coordinates for each.

Under the hood it uses very back-of-the-envelope math: approximating constellation positions by their centroids; using integer angles; rounding times to the nearest hour; and so on.

The constellation “database” consists of the IAU constellation list plus the Pleiades (for a total of 89 objects), but there are 20-odd constellations I more or less arbitrarily identified as the “prominent” ones, and the search function(s) accept an argument to only return the “prominent” constellations.

Basic usage:

                // Create a calendar with the current date June 22, 1979.
                c = new Calendar(1979, 6, 22, 'EST-5EDT');

                // Create a sky instance centered on Cambridge, Mass.
                sky = new NightSky(42, -71, c);

Having done that, you can get a list of visible constellations via:

                local visible = sky.computeVisible(23, 5, true);

Where the first arg (23 here) is the local hour, the second arg is the “radius” of the horizon in hours of right ascension (5 in this case, indicating 11 total hours, or roughly half the sky, is visible), and the optional third argument indicates whether or not to only return the “interesting” constellations (true in this case, resulting in the shorter list).

Each element in the list returned will be a list containing: the name of the constellation, the constellation’s abbreviation, the right ascension of the center of the constellation, the declination of the center of the constellation, and the “interesting” flag.

These can be used to compute the local alt-az coordinates of the constellation (or rather its center) by calling NightSky.raDecToAltAz(). Continuing the example:

                // Iterate over the list.
                visible.forEach(function(o) {
                        // o[3] and o[4] are the RA and DEC, respectively.
                        // 23 is the local hour we're computing.
                        altAz = sky.raDecToAltAz(o[3], o[4], 23);

                        // o[1] is the constellation name, altAz[1]
                        // is the altitude, altAz[2] is the azimuth.
                        "\n\t<<o[1]>> (<<toString(altAz[1])>>,
                                <<toString(altAz[2])>>)\n ";
                });

In this case the output would be:

   Aquarius (15, 118)
   Aquila (43, 137)
   Bootes (53, 268)
   Capricornus (17, 135)
   Cassiopeia (30, 32)
   Cepheus (27, 15)
   Cygnus (58, 69)
   Draco (55, 331)
   Libra (20, 226)
   Lyra (77, 108)
   Pegasus (24, 85)
   Sagittarius (19, 166)
   Scorpius (20, 194)
   Serpens (Caput) (49, 229)
   Serpens (Cauda) (45, 180)
   Ursa Major (24, 318)
   Virgo (8, 257)

There’s also a NightSky.checkConstellation() method that takes a string and an hour as its arguments and returns true if the string is a name or abbreviation for a constellation that’s currently visible for the given hour:

          // Returns true for our example
          sky.checkConstellation('draco', 23);

          // Returns nil
          sky.checkConstellation('aries', 23);

For my purposes I’m going to want some additional sugar for adding constellations into descriptions of the sky, and some actions for handling examing the sky and constellations, but I don’t know if that’ll end up in the module or not.

12 Likes

No way. :joy: You are a wild dev!

8 Likes

Whoever said you were programming the matrix in TADS, they’re probably right.

7 Likes

I’m not surprised about an astronomy routine in TADS, surprises that came from jbg, not joey… :smiley:

Best regards from Italy,
dott. Piergiorgio.

4 Likes

An amazing module. I can imagine numerous use cases.

5 Likes

(@blindhunter) I can imagine exactly one. RISE OF THE MACHINES.

2 Likes

Positively stellar!

3 Likes

:point_left::star_struck::point_left:

2 Likes

Thanks.

It’s worth keeping in mind that the computed positions are very approximate. Basically the thing I want is to be able to keep track of a progression of time (I’m not worried about specific dates, just the idea of something like “the march of time over a single Summer” or something like that), and being able to construct a brief (couple of lines) description of the night sky at various points in time. And then to meaningfully respond to player inputs that reference the description (so if the game described Orion overhead, the parser needs to recognize “orion” and know whether or not it’s currently visible).

Anyway, a couple of updates:

  • The internal ephemeris, such as it is, now uses the Ephem class to hold data related to constellations. So in the examples in the OP, instead of having to know the order of elements in lists, you can just refer to property names.
  • There’s now a computePositions() method that works more or less the same as computeVisible() but computes the alt-az coordinates before returning the list. It also takes an optional callback function as its fourth argument. The callback, if specified, will be called with each constellation’s Ephem instance, and the constellation will be added to the return list only if the callback returns boolean true. Example:
                // Returns a list of the visible constellations including
                // their approximate local alt-az coordinates, for 23:00
                // on the current date (first arg), using the default
                // horizon width (second arg), returning only the "major"
                // constellations (third arg), and only if the computed
                // altitude angle is greater than zero (the callback
                // function)
                l = sky.computePositions(23, nil, true, function(o) {
                        return(o.alt >= 0);
                });

                // Iterate over the list, outputting the names and alt-az
                // coordinates.
                l.forEach(function(o) {
                        "\n\t<<o.name>> (<<toString(o.alt)>>,
                                <<toString(o.az)>>)\n ";
                });
  • Most of the methods have been updated to work better with defaults. Specifically methods that take times will default to using the calendar instance’s current time if an explicit time isn’t given.

And in addition to the above, the calendar and nightSky modules now define global singletons (gameCalendar and gameSky, respectively) and macros for working with them (gCalendar and gSky to refer to them, gSetDate(y, m, d), gSetHour(h), and gSetPosition(lat, long) for working with them).

There’s also a gameEnvironment singleton that can be used to configure the defaults during preinit:

modify gameEnvironment
        currentDate = new Date(1979, 6, 22, 23, 0, 0, 0, 'EST-5EDT')
        latitude = 42
        longitude = -71
;

…will update the global Calendar and global NightSky to use local 23:00 on June 22, 1979 as the date at game start, with the latitude and longitude of Cambridge, Mass.

For testing, both the calendar and nightSky modules provide some debugging actions when compiled with the -d flag (that is, when the __DEBUG preprocessor flag is set):

  • >DEBUG DATE to show the current date and time
  • >SET DATE <year> <month> <day> [<hour>] to set the date and optionally the time (defaults to local midnight if only three arguments are given)
  • >DEBUG POSITION to show the current latitude and longitude
  • >SET POSITION <latitude> <longitude> to set the latitude and longitude
  • >DEBUG SKY to output the current conditions and list the visible constellations and their approximate positions
  • >MAP SKY to output a very rough ASCII art map of the visible constellations
  • >DEBUG TICK to advance the global calendar by one hour

Most of these are probably self-explanatory, but the “map” might need some explanation:

>debug date
It is now 20:00 June 22, 1979.

>debug position
Latitude = 42
Longitude = -71

>map sky
..E..F.............P A: Aquarius
.................... B: Aquila
.............H...... C: Bootes
.................... D: Capricornus
.................... E: Cassiopeia
.................... F: Cepheus
..G................. G: Cygnus
.................... H: Draco
K................... I: Libra
.................C.. J: Lyra
......J............. K: Pegasus
.................... L: Sagittarius
.................... M: Scorpius
...................Q N: Serpens (Caput)
.................... O: Serpens (Cauda)
................N... P: Ursa Major
.................... Q: Virgo
A.B.................
....................
D....L...O...M.....I

>

First, the “normal” parts: the upper edge of the “map” is facing north, the lower edge is facing south, the left edge is facing east, and the right edge is facing *west. This is a common sky map orientation: the idea is that if you held the map overhead you could orient the map to the compass points.

The weird part is that the ASCII map isn’t circular, and the center of the map is celestial north instead of local zenith. This means there’s a lot of distortion, even beyond what’s introduced by the approximations used in the underlying calculations. This in turn means that constellations will sometimes overlap and the position of adjacent constellations will sometimes appear reversed (more likely the more elongated the constellation is, because the calculated positions are based only on the rough geometric center of the constellation).

I guess I could knock together a higher-accuracy version of the module if there’s interest, but the bottom line here is that the current module’s intent is not to provide high-quality positional information about the constellations. It’s just supposed to be a quick and dirty way to answer stuff like “what’s an easily-identifiable constellation overhead?” and “what’s an easily-identified constellation near the horizon to the north?” and so on.

2 Likes

Little bump. Even though the map thing is just a quick hack for debugging, it was bothering me so it now “correctly” outputs a rough ASCII-art approximation of a circular map. Here’s the same section of sky from the example above:

>map sky
.......:::::::....... A: Aquarius
.....:::.....:::..... B: Aquila
....:...........:.... C: Bootes
...:....F........:... D: Capricornus
..:...E...........:.. E: Cassiopeia
.:.............P...:. F: Cepheus
.:.................:. G: Cygnus
::..........H......:: H: Draco
:...................: I: Libra
:..K...G............: J: Lyra
:........J....C.....: K: Pegasus
:...................: L: Sagittarius
:..................Q: M: Scorpius
::...........N.....:: N: Serpens (Caput)
.:.A..B............:. O: Serpens (Cauda)
.:........O.....I..:. P: Ursa Major
..:.D.............:.. Q: Virgo
...:.............:...
....:...L...M...:....
.....:::.....:::.....
.......:::::::.......

The approximate horizon is the circle drawn with colons.

The map still (idiosyncratically) puts celestial north dead center in the map, but the relative positions should more closely approximate those you’d get from a “real” planisphere or astronomy app.

2 Likes

Thought Pain GIF - Find & Share on GIPHY

1 Like

Another update.

The main functional update is the addition of logic to compute the apparent position of the moon. There are several new methods, but the ones most relevant for game dev are:

  • NightSky.getMoonMeridianPosition(h?) returns the approximate lunar position relative to the local meridian, in degrees, in the range -180 to 180. The optional arg is the local hour; the current time (in the Calendar instance used by the NightSky instance) is used if no argument is given.
  • NightSky.getMoon(h?) returns an Ephem instance describing the moon for the given local hour (or the current-for-the-instance time if none is given). This will include the approximate alt-az coordinates for the local sky.

A couple of notes on the lunar position computations: like the rest of the module, they’re very rough approximations intended only to evaluate whether or not the player could see something at a given time and place.

In this case I’m basically simplifying a low-accuracy model in a fairly ad-hoc way (basically just using the first terms in a series expansion). And I’m not bothering to compute the lunar declination at all: it varies between +/- 28.725, but the module uses a constant value (23 by default).

I haven’t bothered to do a bunch of detailed testing against a “real” ephemeris, but spot checking it seems to get moonrise and moonset on random dates in the late 20th/early 21st Century accurate to within an hour, which is more than enough for what I need.

I also updated the debugging tools to include the apparent position of the moon, and in the process of that refined the ASCII mapping tool again. It now outputs a wider map, which (at least in the terminal window I’m running frobTADS in) makes the ASCII map more approximately circular. And making the map wider provides more space, so the markers on the map are now the constellation abbreviations. Here’s the same example used above (twice):

>map sky
..............*************.............. Aqr: Aquarius
..........****.............****.......... Aql: Aquila
.......***.....................***....... Boo: Bootes
.....***.......Cep...............***..... Cap: Capricornus
....*.......Cas.....................*.... Cas: Cassiopeia
..**.........................UMa.....**.. Cep: Cepheus
.**...................................**. Cyg: Cygnus
.*.....................Dra.............*. Dra: Draco
*.......................................* Lib: Libra
*...Peg.....Cyg.........................* Lyr: Lyra
*...............Lyr........Boo..........* Peg: Pegasus
*.......................................* Sgr: Sagittarius
*....................................Vir* Sco: Scorpius
.*........................Ser..........*. Ser: Serpens (Caput)
.**.Aqr.....Aql.......................**. Ser: Serpens (Cauda)
..**...............Ser........Lib....**.. UMa: Ursa Major
....*...Cap.........................*.... Vir: Virgo
.....***.........................***.....
.......***.....Sgr.....Sco.....***.......
..........****.............****..........
..............*************..............

>

I don’t know how many bugs there are to fix, but I think this is all I’m going to do with the module, at least in terms of modelling.

I’m still not sure if all of the stuff for handling vocabulary, descriptions, and so on will end up in this module or will be bespoke code for the WIP.

4 Likes

One more minor addition: I added a low-precision solar ephemeris that works the same way the moon stuff already did: getSun() to get the Ephem instance, getSunMeridianPosition() to get the sun’s current position (in degrees) relative to the local meridian, and so on.

I also created new classes for lunar and solar ephemeris data: MoonEphem and SunEphem. That’s where the RA/DEC stuff lives (the alt-az calculations are still in NightSky, because they’re specific to the latitude/longitude, while the RA/DEC depend only on the date).

There’s also a separate ephemeris element for Polaris, just to help visually verify that the ASCII stuff is working correctly now.

2 Likes

a separate ephemeris for Polaris isn’t just a visual verification of code, at least when the story’s setting is north of the Equator…

Best regards from Italy,
dott. Piergiorgio.

Okay, I just saw this and…

Huh??? Wow. Whaaaat?

2 Likes

The power of TADS!! :star_struck:
Echo: The power of TADS!!

5 Likes

IKR! Like, I really wanna learn it (probably my current WIP would have been absolutely perfect for TADS), but… It’s so confusing!

2 Likes

The man who taught himself ZIL is daunted by TADS?! I find that hard to believe!

6 Likes

It is in the sense that in the present implementation it’s just a debugging UI element, not a searchable object.

It does occur to me that having stars (instead of just constellations) be searchable would be useful as well, so I’ve done some more tinkering.

First, object catalogs now get their own class, NightSkyCatalog. They’re containers for Ephem instances.

The old IAU Designated Constellations table in NightSky is now a standalone catalog instance called iauConstellations, with the catalog ID “iau”. There’s also a new bright star catalog consisting of the 50-odd stars with the highest (numerically small) apparent visual magnitude. It’s called brightStars, catalog ID stars.

You can now change catalogs via NightSky.setCatalog(), i.e. gSky.setCatalog(brightStars) to change the global sky’s catalog to be the bright star catalog. There’s also a new debugging command, >SET CATALOG [catalog ID] to change the catalog. So, to revisit the skies over Cambridge, Mass on the night of June 22, 1979 yet again:

>map sky
Time: Fri Jun 22 21:00:00 1979
............*************............ *:   Polaris
........*****...........*****........ Aql: Aquila
......**.....................**...... Aqr: Aquarius
....**.........................**.... Boo: Bootes
...*.......Cas....*.......UMa....*... Cas: Cassiopeia
..*...............................*.. Cyg: Cygnus
.*.................................*. Lib: Libra
**.................................** Lyr: Lyra
*...Peg....Cyg......................* Peg: Pegasus
*..............Lyr......Boo.........* Sco: Scorpius
*...................................* Ser: Serpens (Cauda)
**...............................Vir* Ser: Serpens (Caput)
.*.........Aql.........Ser.........*. Sgr: Sagittarius
..*.Aqr..........Ser..............*.. UMa: Ursa Major
...*.......................Lib...*... Vir: Virgo
....**.........................**....
......**......Sgr...Sco......**......
........*****...........*****........
............*************............

>set catalog stars
Catalog set.

>map sky
Time: Fri Jun 22 21:00:00 1979
............*************............ *:   Polaris
........*****...........*****........ d:   Arcturus
......**.....................**...... e:   Vega
....**.........................**.... l:   Altair
...*..............*..............*... o:   Antares
..*...............................*.. p:   Spica
.*.................................*. r:   Fomalhaut
**.................................** s:   Deneb
*...........s.......................* x:   Shaula
*...............e............d......*
*...................................*
**.................................**
.*..........l.....................p*.
..*...............................*..
.r.*.............................*...
....**...................o.....**....
......**.....................**......
........*****.....x.....*****........
............*************............

Another minor tweak is that methods that previously took a boolean “notable” flag now take a numeric limit. Each object has a numeric “order” in their catalog (defaulting to 99), with lower numeric values denoting more notable objects.

There’s also a simpler syntax for declaring catalogs, which ought to be obvious from an snippet of the IAU catalog declaration:

iauConstellations: NightSkyCatalog 'iau' 'IAU Designated Constellations';
+Ephem 'Andromeda' 'And' ra = 1 dec = 37;
+Ephem 'Antlia' 'Ant' ra = 10 dec = -32;
+Ephem 'Apus' 'Aps' ra = 16 dec = -75;
+Ephem 'Aquarius' 'Aqr' ra = 22 dec = -9;
+Ephem 'Aquila' 'Aql' ra = 20 dec = 3;
...
+Ephem 'Volans' 'Vol' ra = 8 dec = -68;
+Ephem 'Vulpecula' 'Vul' ra = 20 dec = 24;
+Ephem 'Pleiades' 'M45' ra = 4 dec = 24;
+EphemOrder [ 'Orion', 'Ursa Major', 'Cassiopeia', 'Cygnus', 'Leo',
        'Canis Major', 'Aquarius', 'Gemini', 'Pisces', 'Aries',
        'Aquila', 'Bootes', 'Libra', 'Lyra', 'Pegasus', 'Perseus',
        'Sagittarius', 'Serpens (Caput)', 'Serpens (Cauda)',
        'Scorpius', 'Taurus', 'Virgo', 'Pleadies', 'Andromeda', 'Aquarius',
        'Cancer', 'Capricornus', 'Cepheus', 'Draco' ];

The +Ephem syntax declares an Ephem instance to add to the lexical parent.

The EphemOrder declaration at the end defines the set of “notable” objects in the catalog, and their order property will be set to be their index in the list (objects not in the EphemOrder declaration will keep the default value).

The numeric order can of course be declared on the Ephem instance itself as well, as seen in this snippet from the bright stars catalog:

brightStars: NightSkyCatalog 'stars' 'Arbitrary Bright Star Catalog';
+Ephem 'Sirius' 'a' ra = 7 dec = -16 order = 1;
+Ephem 'Canopus' 'b' ra = 6 dec = -52 order = 2;
+Ephem 'Rigil Kentaurus' 'c' ra = 15 dec = -60 order = 3;
+Ephem 'Arcturus' 'd' ra = 14 dec = 19 order = 4;

Finally, you can also use one of the stock catalogs and just add additional objects you care about to the sky instance directly.

For example, if you wanted to use the stock IAU constellations with the single addition of Arcturus, you can just add Ephem instances to the gameEnvironement singleton:

// Set the current time to be June 22, 1979, @ 23:00 Eastern, and
// the location is Cambridge, Mass.
modify gameEnvironment
        currentDate = new Date(1979, 6, 22, 23, 0, 0, 0, 'EST-5EDT')
        latitude = 42
        longitude = -71
;
// Add an data for Arcturus.
+Ephem 'Arcturus' 'ARC' ra = 14 dec = 19 order = 1;

…then…

>map sky
Time: Fri Jun 22 21:00:00 1979
............*************............ *:   Polaris
........*****...........*****........ Aql: Aquila
......**.....................**...... Aqr: Aquarius
....**.........................**.... ARC: Arcturus
...*.......Cas....*.......UMa....*... Boo: Bootes
..*...............................*.. Cas: Cassiopeia
.*.................................*. Cyg: Cygnus
**.................................** Lib: Libra
*...Peg....Cyg......................* Lyr: Lyra
*..............Lyr......Boo.ARC.....* Peg: Pegasus
*...................................* Sco: Scorpius
**...............................Vir* Ser: Serpens (Caput)
.*.........Aql.........Ser.........*. Ser: Serpens (Cauda)
..*.Aqr..........Ser..............*.. Sgr: Sagittarius
...*.......................Lib...*... UMa: Ursa Major
....**.........................**.... Vir: Virgo
......**......Sgr...Sco......**......
........*****...........*****........
............*************............

>
4 Likes

Oh, but ZIL is so fun! And it fits with my way of thinking - most things are in lists, with ordered sets that become smaller and smaller subsets inside subsets inside subsets to make everything neat.

Yeah, okay, it seems hard, but it’s really not - at least up until you set foot in the treacherous New-Parser land. Then it’s Zarfian Scale Cruel.

But TADS is my next wish. I want to start learning it. My next game, I shall say.

4 Likes