A TADS3/adv3 module for handling calendars and calendar-based cycles

Another little module, this one for handling calendar-based cycles. Right now it specifically handles calculating seasons and the phase of the moon: calendar github repo.

Under the hood the module uses TADS3’s Date class to keep track of the current date.

Seasons are computed based on fixed-date solstices and equinoxes (which will be accurate to +/- one day).

The phase of the moon is computed using the same approximation method used by nethack. This treats the phase as an integer between 1 and 8, with 1 being a new moon and 4 being a full moon. It should also be accurate to within a day.

For my usage I need something that keeps track of the flow of time, but I’m not concerned about a specific in-game year/date.

Usage is pretty simple, here’s a snippet from the demo “game”:

        newGame() {
                local c, i;

                // Create a calendar with a current date of June 22, 1979.
                c = new Calendar(1979, 6, 22);

                // Loop through a hundred days.
                for(i = 1; i <= 100; i++) {
                        // Output information about the current date.
                        _logDate(c);

                        // Advance the calendar's date by one day.
                        c.advanceDay();
                }
        }

        // Log some stuff about the date:
        _logDate(d) {
                "Date: <<d.getMonthName()>> <<toString(d.getDay())>>,
                        <<toString(d.getYear())>>\n ";
                "Season: <<d.getSeasonName()>>\n ";
                "Phase of moon: <<toString(d.getMoonPhaseName())>>\n ";
                "<.p> ";
        }

This just creates a calendar with the date June 22, 1979, and loops over the next 100 days, outputting information about each date. Partial transcript:

Date: June 22, 1979
Season: summer
Phase of moon: new

Date: June 23, 1979
Season: summer
Phase of moon: new

Date: June 24, 1979
Season: summer
Phase of moon: new

Date: June 25, 1979
Season: summer
Phase of moon: new

Date: June 26, 1979
Season: summer
Phase of moon: waxing crescent

Date: June 27, 1979
Season: summer
Phase of moon: waxing crescent

Date: June 28, 1979
Season: summer
Phase of moon: waxing crescent

Date: June 29, 1979
Season: summer
Phase of moon: waxing crescent

Date: June 30, 1979
Season: summer
Phase of moon: first quarter

Date: July 1, 1979
Season: summer
Phase of moon: first quarter

Date: July 2, 1979
Season: summer
Phase of moon: first quarter

Date: July 3, 1979
Season: summer
Phase of moon: first quarter

Date: July 4, 1979
Season: summer
Phase of moon: waxing gibbous

This is another one where I dunno if it’ll help anyone else, but here it is.

5 Likes

So, based on the incredibly thorough and intricate library functionality you have shared to date, I am concluding that your WIP IMPLEMENTS ALL OF CREATION.

Was going to say “can’t wait to see it,”… unless I’m living it already!? (And if I am, what is the code to disable the MAGA mod?)

3 Likes

A little update.

The Calendar class now provides a getSiderealTime() method which returns the approximate Greenwich sidereal time at local midnight for the current (for the Calendar instance) date, and a getLocalSiderealTime(hour?, longitude?) method that returns the approximate local sidereal time for the given local hour (first arg) and longitude (second arg).

The module now caches computed “stuff” (like the season, lunar phase, and sidereal time at midnight), clearing the cached values when the Calendar instance’s date changes (and caching them when computed).

1 Like

Mid-sized update.

The calendar module now implements daily cycles with fixed “slots”. This is for games where there are discrete “blocks” of time during the day, but individual actions/turns don’t advance the game clock from one block to another.

This involves two classes, DailyCycle and Period. Basically a Period is an interval during the day and a DailyCycle is a container for one or more Periods.

To just jump in with an example, here’s one daily cycle provided by the module (in calendarCycles.t):

simpleDay: DailyCycle;
+Period 'early' 'Early Morning' +4;
+Period 'morning' 'Morning' +8;
+Period 'afternoon' 'Afternoon' +12;
+Period 'evening' 'Evening' +19;
+Period 'night' 'Nighttime' +22;

Each Period is an ID, an optional name, and a starting hour.

Periods run from their stated starting hour until the next declared period. So in this example early runs from 04:00 to 07:00, because the next period after it is morning starting at 08:00.

Periods automatically “wrap” around the end of the day, so in this case night runs from 22:00 (the declared starting hour) until 03:00 (the start of early).

METHODS

There are a bunch of methods for working with Periods on DailyCycle, but you probably just want to use the ones defined directly on Calendar:

  • Calendar.setDailyCycle(obj) sets the calendar’s daily cycle. the simpleDay cycle described above is the default. The module also provides canonicalHours, a daily cycle for the medieval canonical hours.
  • Calendar.setPeriod(id) sets the calendar’s current hour to be the start of the given period
  • Calendar.setDateAndPeriod(year, month, day, id) sets the calendar’s current date and time
  • Calendar.setPeriodNextDay(id) advances the calendar’s current time to be the given period during the following day
  • Calendar.matchPeriod(h?) returns the ID of the period matching the given hour for the current day. if no argument is given, the current hour is used
1 Like

A minor update.

You can now compile with the -D CALENDAR_EVENTS flag to enable calendar events. When enabled, objects can subscribe to a calendar object using the eventHandler interface.

AN EXAMPLE

For an object to use event subscriptions, you need to add the EventListener mixin to its class list.

By default, the object’s eventHandler() method will be called when a matching event is fired. The method must accept one argument (for an event object).

        // Declare an object with the EventListener class.
        pebble: Thing, EventListener '(small) (round) pebble' 'pebble'
                "A small, round pebble. "

                eventHandler(e) {
                        "<.p>This is a useless event handler.<.p> ";
                }
        ;

Having done this, the object can subscribe for calendar notifications via:

        gCalendar.subscribe(pebble);

Having done this pebble.eventHandler() will be called whenever the date/time of the global calendar object changes. The argument to the handler in this case will be the new date (an instance of the TADS3 builtin class Date).

I’m in the process of implementing logic for declaring daily schedules for NPCs, so my use case here is that calendar updates are based around the module’s standard daily cycle: “afternoon” becomes “evening”, “evening” becomes “night”, and so on, with transitions being due to event triggers instead of turn counts. As implemented CalendarEvent is probably less useful if you want “continuous time” (each turn takes n minutes or whatever).

The logic is provided by the eventHandler module and it supports arbitrary events, so in theory you could add fancier logic (like firing different events for hour, day, and so on changes) but I don’t have any plans to do so myself.

1 Like

if someone want to write First Things First II now has the main tool for handling timetravel… and the passing of time between time travelling :slight_smile:

Best regards from Italy,
dott. Piergiorgio.

Hey jbg, this is very cool! In fact, I plan to create a puzzle that involves a kind of time travel to a moment in time that has a full-moon, so your module is exactly what I would be looking for anyway.
Could you give me a short hint on how to import it to an existing project? It is the first module I will use. Note that I do not use the workbench and code on Linux. Maybe you have some tipps for me :slight_smile:
Thank you for your effort, and keep up the great work !

1 Like

Sure. I’ll assume you understand generally how file and path names work in linux, but let me know if you need anything clarified.

The way I handle this is I have a directory for all my TADS stuff, and projects (i.e. games) and modules are subdirectories of it.

So if the base TADS directory I’m working out of is /home/jbg/tads/, I’d go there and clone the repo:

> cd /home/jbg/tads
> git clone https://github.com/diegesisandmimesis/calendar

That’ll create /home/jbg/tads/calendar/.

In my case the game I’m developing is tentatively called Sweet Thunder, so it’s in /home/jbg/tads/thunder/. In order to add the module to the project, I edit the makefile to:

  • Add the path to all modules via -Fs. This has to be near the top of the makefile, before any of the -lib lines
  • Add the module itself via -lib. This has to be near the end of the makefile
  • Additionally add any preprocessor flags for the module via -D. Placement of these is more flexible, but I put them near the top of the file just to make them easier to find/edit

You can specific fully-qualified paths to directories and/or files, but I generally use relative paths to make things more portable. So if the module is in /home/jbg/tads/calendar/ and the game is in /home/jbg/tads/thunder/, then the makefile.t3m looks something like:

-I .
-D LANGUAGE=en_us
-D MESSAGESTYLE=neu
-Fy obj
-Fo obj
-Fs ..
[other makefile stuff]
-lib calendar/calendar

The lines at the top, before the -Fs line, are just general T3 makefile stuff, and yours might look slightly different.

The -Fs .. means to search for files using .. as the base path. From the game directory, /home/jbg/tads/thunder/, that means /home/jbg/tads/, the top-level directory for the modules.

The -lib line is what actually includes the module, and -lib calendar/calendar means that, relative to the path specified by -Fs, look in a subdirectory called calendar for a file called calendar.tl. You could also use -lib calendar/calendar.tl, but t3make assumes the extension if one isn’t specified.

The .tl file is just the equivalent of the makefile for the module (I believe the extension means “TADS library”, but I’m not sure). It’s just a list of all the source files in the module, and is used by t3make to figure out what to add to the project.

If you’re compiling with the -D CALENDAR_EVENTS flag you’ll also need to install the eventHandler module the same way:

> cd /home/jbg/tads
> git clone https://github.com/diegesisandmimesis/eventHandler

…and then add a line to your makefile:

-lib eventHandler/eventHandler

…just before the -lib calendar/calendar line.

Each module has a ./demo/ subdirectory with demos/test cases, and each of them has a makefile that can probably be used as a reference (although the demos are designed to be compiled from the ./demo/ subdirectory, and so use -Fs ../.. instead of -Fs .. because they’re one level deeper).

2 Likes