diff --git a/CLAUDE.md b/CLAUDE.md index 0a70328..3fcc67b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,10 @@ deck_in_a_dash/ res/ Tarot card art (22 RWS Major Arcana JPEGs), Waite's "Pictorial Key to the Tarot" (PDF, public domain — source of all card/spread - text), logo, research notes. + text), Sepharial's "Transits and Planetary + Periods" (PDF, public domain — source of + interpreter/src/narrative.c's text), logo, + research notes. engine/ third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT, pinned commit — see VENDORED.md). @@ -199,11 +202,14 @@ into the Pebble watchapp unchanged later. Only `reading_print_text`/ A second, separate top-level module and **its own binary** (`dist/interpreter-cli`, sibling to `dist/deck-engine`) that scores *how -significant a day's transits are* — the first piece of a larger "turn the -raw reading into an actual narrative" effort described in project chat -history, not yet producing any narrative text. `deck-engine` itself is -deliberately unchanged by this beyond gaining `--format json`; the two -binaries compose over that JSON, never by linking together: +significant a day's transits are* and, for each of the top items, prints +a short narrative sentence about what that transit classically means — +the first piece of a larger "turn the raw reading into an actual +narrative" effort described in project chat history; the Celtic Cross +spread itself isn't woven into that narrative yet (see +`docs/reading.md`). `deck-engine` itself is deliberately unchanged by +this beyond gaining `--format json`; the two binaries compose over that +JSON, never by linking together: ```bash make # -> dist/deck-engine (unchanged; --format json is new) and dist/interpreter-cli @@ -223,11 +229,15 @@ between the two binaries (see the next bullet). every aspect in `reading->transits.aspects[]`, keeps the **top 5** by score in `DailyInterpretation.top_items[]` (descending, evicting the rest), and classifies the day into a `DaySignificance` enum (`DAY_QUIET` - ... `DAY_MAJOR`) from the single highest-scoring item. - `interpretation_deserves_framing()` is true only at `DAY_MAJOR` — the - intended hook for giving the Celtic Cross reading a "this matters" - intro sentence naming `top_items[0]` on days that earn it; that - rendering doesn't exist yet, `main.c`'s report is plain text only. + ... `DAY_MAJOR`) from the single highest-scoring item. The enum also + carries a trailing `DAY_SIGNIFICANCE_COUNT` sentinel (not a real level) + purely so callers can print the level as a rank, e.g. `main.c`'s + `"%s (%d/%d)"` → `"Notable (2/4)"` — `interp.day_level + 1` out of + `DAY_SIGNIFICANCE_COUNT`. `interpretation_deserves_framing()` is true + only at `DAY_MAJOR` — the intended hook for giving the Celtic Cross + reading a "this matters" intro sentence naming `top_items[0]` on days + that earn it; that rendering doesn't exist yet, `main.c`'s report is + plain text only. - Score = `transiting-planet weight (slow/outer planets score higher) × orb tightness (1.0 = exact, ~0 = at the edge) × natal-luminary bonus (transits to natal Sun/Moon score higher than to other planets)`. The @@ -235,6 +245,29 @@ between the two binaries (see the next bullet). thresholds in `significance.c` are hand-tuned, not derived from anything physical — expect to retune them once real readings are compared against how "major" a day actually feels. +- **`narrative.c`/`narrative.h`** supplies `main.c`'s one-sentence + description printed under each top item, condensed from Sepharial's + *Transits and Planetary Periods* (1920, public domain — + `res/Transits_and_Planetary_Periods.pdf`), Chapter VIII "Effects of + Transits". Per transiting planet it holds a `base` description plus a + `harmonious`/`discordant` addendum, shown only under a trine/sextile or + square/opposition respectively (either may be `""` if the source gives + none); a conjunction is treated as neutral — base only — since + Sepharial's own text says a conjunction "takes the nature of what it + conjoins," which isn't modelled here. The Moon and Pluto aren't in that + chapter (fast lunar transits were out of scope for a year-ahead + forecasting book; Pluto wasn't discovered until 1930) — their text is + original, written for this project, not drawn from Sepharial. Each + narrative is also framed by *where* it's happening: `narrative_print()` + takes the transiting planet's whole-sign house (1-12, relative to the + natal chart, same as everywhere else in this codebase) and prefixes an + "In matters of ..." line naming that house's traditional area of life, + from a small `k_house_area[12]` table of standard whole-sign house + significations — centuries-old convention, not attributed to any single + source (same status as the sign/aspect names already in `astro.c`). + Passing a house outside 1-12 (the same "no data" sentinel `reading_io.c` + uses elsewhere) omits that framing and prints the planet's narrative on + its own. - **`reading_print_json`** (`engine/src/reading.c`) is the wire format: the full `DailyReading` (natal, transits, spread), using the language-independent `astro_*_slug()`/`tarot_*_slug()` accessors @@ -261,10 +294,11 @@ between the two binaries (see the next bullet). it before printing sign/house), since that's supplementary display context rather than something scoring depends on. - **Deliberately header-only dependency on the engine, throughout**: - `significance.h`/`reading_io.h` `#include engine/src/reading.h` for the - struct/enum *definitions*, but the root `Makefile` never compiles or - links any engine `.c` file (or the vendored Astronomy Engine) into the - interpreter's binary or tests — every + `significance.h`/`reading_io.h`/`narrative.h` `#include` engine headers + (`reading.h`/`astro.h`) for the struct/enum *definitions*, but the root + `Makefile` never compiles or links any engine `.c` file (or the + vendored Astronomy Engine) into the interpreter's binary or tests — + every test in `interpreter/tests/` runs against hand-built JSON text or hand-built `DailyReading` values, with no real ephemeris/tarot-draw call involved anywhere. Consequently `reading_io.c` (planet/aspect/sign @@ -287,3 +321,7 @@ between the two binaries (see the next bullet). - `engine/i18n/de.lang`'s card/spread text is an original translation of that same public-domain English text, made for this project — not taken from any specific published German edition. +- Transit narrative text in `interpreter/src/narrative.c`: condensed from + Sepharial's *Transits and Planetary Periods* (1920, public domain — + `res/Transits_and_Planetary_Periods.pdf`), except the Moon and Pluto + entries, which are original (see narrative.c's own comment for why). diff --git a/Makefile b/Makefile index 986ca06..7bd7f32 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,8 @@ ENGINE_TEST_BINARY := $(BUILD_DIR)/smoke_test INTERP_SRC_DIR := interpreter/src INTERP_TEST_DIR := interpreter/tests -INTERP_LIB_SRCS := $(INTERP_SRC_DIR)/significance.c $(INTERP_SRC_DIR)/json.c $(INTERP_SRC_DIR)/reading_io.c +INTERP_LIB_SRCS := $(INTERP_SRC_DIR)/significance.c $(INTERP_SRC_DIR)/json.c \ + $(INTERP_SRC_DIR)/reading_io.c $(INTERP_SRC_DIR)/narrative.c INTERP_LIB_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(INTERP_LIB_SRCS)) INTERP_MAIN_OBJ := $(OBJ_DIR)/$(INTERP_SRC_DIR)/main.o @@ -54,8 +55,10 @@ INTERP_BINARY := $(DIST_DIR)/interpreter-cli SIGNIFICANCE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/significance_test.o JSON_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/json_test.o +NARRATIVE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/narrative_test.o SIGNIFICANCE_TEST_BIN := $(BUILD_DIR)/significance_test JSON_TEST_BIN := $(BUILD_DIR)/json_test +NARRATIVE_TEST_BIN := $(BUILD_DIR)/narrative_test .PHONY: all test clean images i18n scripts @@ -106,10 +109,11 @@ scripts: cp $(ENGINE_SCRIPTS_DIR)/user.properties.template $(DIST_DIR)/user.properties; \ fi -test: $(ENGINE_TEST_BINARY) $(SIGNIFICANCE_TEST_BIN) $(JSON_TEST_BIN) +test: $(ENGINE_TEST_BINARY) $(SIGNIFICANCE_TEST_BIN) $(JSON_TEST_BIN) $(NARRATIVE_TEST_BIN) ./$(ENGINE_TEST_BINARY) ./$(SIGNIFICANCE_TEST_BIN) ./$(JSON_TEST_BIN) + ./$(NARRATIVE_TEST_BIN) $(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ) $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) @@ -120,6 +124,9 @@ $(SIGNIFICANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o $(SIGNIFIC $(JSON_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/json.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/reading_io.o $(JSON_TEST_OBJ) $(CC) $(CFLAGS) -o $@ $^ +$(NARRATIVE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/narrative.o $(NARRATIVE_TEST_OBJ) + $(CC) $(CFLAGS) -o $@ $^ + $(OBJ_DIR)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) -c -o $@ $< diff --git a/README.md b/README.md index 518dd3b..709b039 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ dropping a translated `key=value` file into `engine/i18n/` — see ``` res/ Tarot card art (22 RWS Major Arcana), the - Waite PDF, logo, research notes. + Waite and Sepharial PDFs, logo, research notes. engine/ third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT). src/ Engine source (rng, tarot, astro, i18n, reading, CLI). @@ -45,7 +45,8 @@ engine/ the user.properties template. dist/ Build output (gitignored) — see below. interpreter/ Separate module + binary (dist/interpreter-cli): - scores how significant a day's transits are, + scores how significant a day's transits are + and prints a short narrative note for each, reading deck-engine's --format json output (see CLAUDE.md). docs/ Architecture/dataflow diagrams, input/output format reference. @@ -118,3 +119,8 @@ unmodified) — see its `VENDORED.md` for the pinned commit. Card and spread text in `engine/src/tarot_data.c` is condensed from A. E. Waite's *The Pictorial Key to the Tarot* (1911), which is in the public domain. + +Transit narrative text in `interpreter/src/narrative.c` is condensed +from Sepharial's *Transits and Planetary Periods* (1920), also in the +public domain, except the Moon and Pluto entries, which are original +(see the file's own comment). diff --git a/docs/input-output-format.md b/docs/input-output-format.md index f3cf20c..2fe3784 100644 --- a/docs/input-output-format.md +++ b/docs/input-output-format.md @@ -248,18 +248,26 @@ dist/run-interpreter.sh `house`, is silently skipped rather than failing the load (see `parse_bodies()` in `reading_io.c`); `spread` is always ignored. - **Output**: a plain-text report — the day's overall significance level - (`Quiet`/`Notable`/`Significant`/`Major`), a "worth a deeper Celtic - Cross look" line on `Major` days only, and up to 5 of today's aspects - ranked by score, each naming the sign and natal house the transiting - planet currently occupies and the sign and house of the natal planet - it's aspecting: + (`Quiet`/`Notable`/`Significant`/`Major`, plus its rank out of the 4 + defined levels, e.g. `Notable (2/4)`), a "worth a deeper Celtic Cross + look" line on `Major` days only, and up to 5 of today's aspects ranked + by score, each naming the sign and natal house the transiting planet + currently occupies and the sign and house of the natal planet it's + aspecting, followed by a short narrative sentence about what that + transiting planet classically signifies *and which area of life its + current house governs* (see `CLAUDE.md`'s `narrative.c` bullet) — + omitted only if the source material has nothing to say for that + planet/aspect combination: ``` - Day significance: Significant + Day significance: Significant (3/4) (a major transit today - worth a deeper Celtic Cross look) Top 3 significant transits: 1. Transiting Saturn in Aries (house 2) Square natal Sun in Taurus (house 3) (orb 0.5°, score 8.10) + In matters of money, possessions, and personal values (house 2). Brings depression, stagnation, hindrances and obstacles, and some deprivation of the usual benefits. 2. Transiting Pluto in Aquarius (house 12) Opposition natal Moon in Capricorn (house 11) (orb 0.2°, score 7.92) + In matters of solitude, the subconscious, and hidden matters (house 12). Brings deep, often hidden transformation - the surfacing or dismantling of something that has outgrown its old form. 3. Transiting Jupiter in Leo (house 5) Trine natal Venus in Aries (house 2) (orb 2.1°, score 3.40) + In matters of romance, creativity, and children (house 5). Brings increase and expansion - fullness of fortune and health, and a generally fortunate time. ``` diff --git a/docs/reading.md b/docs/reading.md index 1cb213b..39b83eb 100644 --- a/docs/reading.md +++ b/docs/reading.md @@ -75,26 +75,92 @@ spread — the same source most modern tarot decks trace back to. Not every day is equally eventful astrologically. A separate, optional step can look at today's aspects (part 2, above) and works out an overall **significance level** for the day — Quiet, Notable, -Significant, or Major — based on which planets are involved, how exact -the angle is, and whether it touches your Sun or Moon (the two most -personally weighted points in a chart). On a genuinely Major day, it -also calls out the single most important thing happening, as a hint -that it's worth paying closer attention to your tarot spread that day -rather than treating it as routine. Quiet days still get a full +Significant, or Major, shown together with its rank out of those four +(for example "Notable (2/4)") — based on which planets are involved, +how exact the angle is, and whether it touches your Sun or Moon (the +two most personally weighted points in a chart). On a genuinely Major +day, it also calls out the single most important thing happening, as a +hint that it's worth paying closer attention to your tarot spread that +day rather than treating it as routine. Quiet days still get a full reading — this step only adds a "pay attention today" flag, it never removes anything. +For each of the handful of events that make up today's score, you also +get a plain sentence about what it classically means and *which area +of your life* it's playing out in — drawn from the house it falls in +(see "Today's sky", above). For example, a tense aspect from Saturn +landing in your 6th house reads as a day for your daily work and +routine to feel a bit stuck, not just "Saturn is doing something." +That wording is condensed from a public-domain 1920 astrology book +(Sepharial's *Transits and Planetary Periods*), the same way the tarot +card meanings are condensed from Waite's 1911 book. + ## What a day's reading actually gives you Put together, one day's reading contains: your natal chart (for reference), today's sky and how it's speaking to your chart, how -significant today is overall, and your 10-card Celtic Cross spread for -the day. Right now these sit side by side — the astrology tells you -*how eventful* today is and *where* to pay attention, and the tarot -spread gives you the actual guidance to sit with. They don't yet get -woven into a single narrative (e.g. a spread explained *in light of* -today's significant transit) — that combined storytelling is the next -piece to build, not something you get today. +significant today is overall (with a plain-language note on each of the +main events driving that), and your 10-card Celtic Cross spread for the +day. Right now these sit side by side — the astrology tells you *how +eventful* today is and *where* to pay attention, and the tarot spread +gives you the actual guidance to sit with. They don't yet get woven +into a single narrative (e.g. a spread explained *in light of* today's +significant transit) — that combined storytelling is the next piece to +build, not something you get today. *(This page, like that piece, is still a work in progress — expect it to grow as the reading itself does.)* + +## A worked example + +Here's what an actual day's reading looks like end to end, for someone +born 14 March 1988 in Berlin, checking their reading on 5 July 2026. + +**Natal chart (excerpt).** Sun in Pisces, Moon in Aquarius, Ascendant in +Sagittarius. + +**Today's sky (excerpt).** Moon phase: Waning Gibbous. Several planets +are forming aspects back to the natal chart above — see below for which +ones actually matter. + +**How big is today.** + +``` +Day significance: Major (4/4) +(a major transit today - worth a deeper Celtic Cross look) + +Top 5 significant transits: + 1. Transiting Pluto in Aquarius (house 3) Conjunction natal Moon in Aquarius (house 3) (orb 2.9°, score 9.60) + In matters of communication, siblings, and everyday learning (house 3). Brings deep, often + hidden transformation - the surfacing or dismantling of something that has outgrown its old form. + 2. Transiting Neptune in Aries (house 5) Sextile natal Moon in Aquarius (house 3) (orb 2.5°, score 9.25) + In matters of romance, creativity, and children (house 5). Brings a state of chaos and confusion - + an involved, uncertain condition of affairs, with plots, subtlety, or unseen influences at work. + 3. Transiting Uranus in Gemini (house 7) Trine natal Moon in Aquarius (house 3) (orb 2.0°, score 8.96) + In matters of partnerships and close relationships (house 7). Brings separations, estrangements, + sudden dislocations and violent upsets. Success through official or civic channels, and beneficial + changes or appointments, are possible. +``` + +In plain terms: today is about as big as this reading gets (Major, +4 out of 4), and the story is squarely about the Moon — a slow-moving +Pluto sitting right on top of it, a chaotic Neptune touching it gently +from the 5th house, and a disruptive-but-possibly-lucky Uranus trine +from the 7th. Communication, romance, and relationships are all +quietly under pressure at once. + +**Tarot spread (excerpt).** Drawn independently, using today's date: + +- *The Present* — The Emperor, reversed: immaturity or a loss of + authority, rather than confident structure. +- *The Challenge* — The Devil: force, ravage, an obstacle that isn't + easily reasoned with. +- *The Near Future* — The World, reversed: stagnation, things staying + fixed in place rather than resolving. + +Read side by side, the astrology and the tarot happen to rhyme here — +both point toward something that wants to shift (Pluto's "hidden +transformation," The Devil's "force") running into something that +feels stuck (a reversed World, an afflicted Moon) — but that's the +reader's own synthesis for now, not something the app does for you (see +above). diff --git a/interpreter/src/main.c b/interpreter/src/main.c index 918203c..77c29d1 100644 --- a/interpreter/src/main.c +++ b/interpreter/src/main.c @@ -3,6 +3,7 @@ #include #include +#include "narrative.h" #include "reading_io.h" #include "significance.h" @@ -12,7 +13,7 @@ static void print_usage(const char *prog) { "Reads a DailyReading in deck-engine's --format json shape - from the\n" "given file, or from stdin if no file is given (or it is \"-\") - and\n" "prints a significance report: the day's overall level and the top\n" - "aspects driving it.\n\n" + "aspects driving it, each with a short narrative note.\n\n" "Typical use:\n" " dist/deck-engine ... --format json | %s\n" " dist/deck-engine ... --format json > reading.json && %s reading.json\n", @@ -115,7 +116,8 @@ int main(int argc, char **argv) { DailyInterpretation interp; interpret_daily_reading(&reading, &interp); - printf("Day significance: %s\n", day_level_name(interp.day_level)); + printf("Day significance: %s (%d/%d)\n", day_level_name(interp.day_level), + interp.day_level + 1, DAY_SIGNIFICANCE_COUNT); if (interpretation_deserves_framing(&interp)) { printf("(a major transit today - worth a deeper Celtic Cross look)\n"); } @@ -137,6 +139,7 @@ int main(int argc, char **argv) { printf(" %s natal %s", aspect_display_name(a->type), body_display_name(a->natal_planet)); print_body_in_sign(&reading.natal.bodies[a->natal_planet]); printf(" (orb %.1f\xc2\xb0, score %.2f)\n", a->orb, item->score); + narrative_print(stdout, " ", a, reading.transits.bodies[a->transiting_planet].house); } return 0; } diff --git a/interpreter/src/narrative.c b/interpreter/src/narrative.c new file mode 100644 index 0000000..e195b9e --- /dev/null +++ b/interpreter/src/narrative.c @@ -0,0 +1,133 @@ +#include "narrative.h" + +#include + +typedef struct { + const char *base; /* always relevant, may be "" (see the Sun) */ + const char *harmonious; /* shown only under a trine/sextile; may be "" */ + const char *discordant; /* shown only under a square/opposition; may be "" */ +} TransitNarrative; + +/* See narrative.h's doc comment for sourcing (Sepharial, "Transits and + * Planetary Periods", 1920, Chapter VIII) and the Moon/Pluto exception. */ +static const TransitNarrative k_narratives[NUM_BODIES] = { + [PLANET_SUN] = { + .base = "", + .harmonious = "Benefits from superiors and advancement in your sphere of " + "life and work - honours, emoluments, and successful new " + "associations.", + .discordant = "Degradation and dishonour, loss of position, and adverse " + "judgement from superiors.", + }, + [PLANET_MOON] = { + /* Original text, not from Sepharial - see narrative.h. */ + .base = "Colours the everyday and domestic sphere of life, often " + "coinciding with the opening of new avenues.", + .harmonious = "These changes tend to be advantageous.", + .discordant = "These changes tend to be adverse, with some indisposition " + "or domestic friction.", + }, + [PLANET_MERCURY] = { + .base = "Affects writings, journeys, commerce, and everyday activities - " + "a neutral messenger whose effect follows the nature of the " + "aspect it makes.", + .harmonious = "", + .discordant = "", + }, + [PLANET_VENUS] = { + .base = "Brings domestic and social affairs to the fore - happiness, " + "comforts, and favours.", + .harmonious = "Success in love affairs and artistic pursuits is likely.", + .discordant = "Grief and disappointment are more likely.", + }, + [PLANET_MARS] = { + .base = "A strenuous time of quarrels, contention, strife and anger, with " + "some risk of hurts or injuries depending on the sign it " + "occupies.", + .harmonious = "Can bring benefits from doctors, surgeons, or new projects " + "and enterprises.", + .discordant = "", + }, + [PLANET_JUPITER] = { + .base = "Brings increase and expansion - fullness of fortune and health, " + "and a generally fortunate time.", + .harmonious = "", + .discordant = "", + }, + [PLANET_SATURN] = { + .base = "Brings depression, stagnation, hindrances and obstacles, and " + "some deprivation of the usual benefits.", + .harmonious = "Favours from older connections and past associations are " + "still possible.", + .discordant = "", + }, + [PLANET_URANUS] = { + .base = "Brings separations, estrangements, sudden dislocations and " + "violent upsets.", + .harmonious = "Success through official or civic channels, and " + "beneficial changes or appointments, are possible.", + .discordant = "", + }, + [PLANET_NEPTUNE] = { + .base = "Brings a state of chaos and confusion - an involved, uncertain " + "condition of affairs, with plots, subtlety, or unseen " + "influences at work.", + .harmonious = "", + .discordant = "", + }, + [PLANET_PLUTO] = { + /* Original text, not from Sepharial - see narrative.h. */ + .base = "Brings deep, often hidden transformation - the surfacing or " + "dismantling of something that has outgrown its old form.", + .harmonious = "", + .discordant = "", + }, +}; + +static bool aspect_is_harmonious(AspectType type) { + return type == ASPECT_TRINE || type == ASPECT_SEXTILE; +} + +static bool aspect_is_discordant(AspectType type) { + return type == ASPECT_SQUARE || type == ASPECT_OPPOSITION; +} + +/* Traditional whole-sign house significations - the "area of life" each + * house governs. This is centuries-old, uncredited astrological + * convention (the same status as the sign/aspect names already baked + * into engine/src/astro.c), not text drawn from Sepharial or any other + * single source. Indexed house - 1. */ +static const char *const k_house_area[12] = { + "your sense of self, identity, and outward appearance", + "money, possessions, and personal values", + "communication, siblings, and everyday learning", + "home, family, and your roots", + "romance, creativity, and children", + "daily work, routine, and health", + "partnerships and close relationships", + "shared resources, intimacy, and transformation", + "travel, higher learning, and beliefs", + "career, reputation, and public standing", + "friendships, community, and hopes for the future", + "solitude, the subconscious, and hidden matters", +}; + +void narrative_print(FILE *out, const char *indent, const Aspect *aspect, + int transiting_house) { + const TransitNarrative *n = &k_narratives[aspect->transiting_planet]; + + const char *extra = ""; + if (aspect_is_harmonious(aspect->type)) extra = n->harmonious; + else if (aspect_is_discordant(aspect->type)) extra = n->discordant; + + if (n->base[0] == '\0' && extra[0] == '\0') return; + + fprintf(out, "%s", indent); + if (transiting_house >= 1 && transiting_house <= 12) { + fprintf(out, "In matters of %s (house %d). ", + k_house_area[transiting_house - 1], transiting_house); + } + if (n->base[0] != '\0') fprintf(out, "%s", n->base); + if (extra[0] != '\0') fprintf(out, "%s%s", n->base[0] != '\0' ? " " : "", extra); + fprintf(out, "\n"); +} diff --git a/interpreter/src/narrative.h b/interpreter/src/narrative.h new file mode 100644 index 0000000..b78fc9b --- /dev/null +++ b/interpreter/src/narrative.h @@ -0,0 +1,46 @@ +#ifndef DECK_NARRATIVE_H +#define DECK_NARRATIVE_H + +#include + +/* Header-only dependency on the engine, same policy as significance.h - + * see its comment for why. */ +#include "../../engine/src/astro.h" + +/* Condensed, public-domain descriptions of what each transiting planet + * classically signifies, sourced from Sepharial's "Transits and + * Planetary Periods" (1920, public domain - res/Transits_and_Planetary_ + * Periods.pdf), Chapter VIII "Effects of Transits" (pp. 60-61). The + * chapter's own structure - a base description of the planet's transit, + * plus a "well aspected"/"badly aspected" addendum - maps directly onto + * transiting-planet + aspect-type, our existing Aspect fields, so no new + * engine data is needed to use it. + * + * The Moon and Pluto aren't covered by that chapter (fast lunar transits + * were outside a year-ahead-forecasting book's scope; Pluto wasn't + * discovered until 1930, a decade after this book) - their text in + * narrative.c is original, written for this project in a matching + * voice, not drawn from Sepharial. + * + * Prints a narrative sentence for `aspect`'s transiting planet to `out`, + * indented with `indent` and followed by a newline - or prints nothing + * if the source material has no text applicable to this planet/aspect + * combination (e.g. an exactly neutral conjunction to a planet whose + * only text is a "well/badly aspected" addendum, like the Sun). A + * conjunction is treated as neutral - neither the "harmonious" nor the + * "discordant" addendum is shown for it - since Sepharial's own text + * says a conjunction "takes the nature of what it conjoins," something + * this data doesn't model. + * + * `transiting_house` is the whole-sign house (1-12) the transiting + * planet currently occupies in the natal chart - i.e. + * reading->transits.bodies[aspect->transiting_planet].house - and names + * *which area of life* the transit is playing out in, the standard, + * personalized way transits are read (see astro.h's own house-field + * comment). Pass a value outside 1-12 (e.g. the "no data" sentinel `0` + * used elsewhere - see reading_io.c's parse_bodies()) to omit that + * framing and print the planet's narrative on its own. */ +void narrative_print(FILE *out, const char *indent, const Aspect *aspect, + int transiting_house); + +#endif diff --git a/interpreter/src/significance.h b/interpreter/src/significance.h index 068d0b8..3a41d92 100644 --- a/interpreter/src/significance.h +++ b/interpreter/src/significance.h @@ -34,7 +34,10 @@ typedef enum { DAY_QUIET = 0, DAY_NOTABLE, DAY_SIGNIFICANT, - DAY_MAJOR + DAY_MAJOR, + DAY_SIGNIFICANCE_COUNT /* not a real level - the number of levels above, + * for callers that want to show day_level as a + * "rank out of N" (e.g. main.c's report). */ } DaySignificance; typedef struct { diff --git a/interpreter/tests/narrative_test.c b/interpreter/tests/narrative_test.c new file mode 100644 index 0000000..ae061ef --- /dev/null +++ b/interpreter/tests/narrative_test.c @@ -0,0 +1,125 @@ +#include +#include +#include + +#include "../src/narrative.h" + +static const char *slurp(FILE *f) { + static char buf[4096]; + long len = ftell(f); + rewind(f); + size_t n = fread(buf, 1, (size_t)len, f); + buf[n] = '\0'; + return buf; +} + +static void test_hard_aspect_shows_base_but_not_harmonious_bonus(void) { + Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON, + .type = ASPECT_SQUARE, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 0); + const char *text = slurp(f); + + assert(strstr(text, "depression") != NULL); + assert(strstr(text, "past associations") == NULL); + + fclose(f); + printf("PASS test_hard_aspect_shows_base_but_not_harmonious_bonus\n"); +} + +static void test_soft_aspect_adds_harmonious_bonus(void) { + Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN, + .type = ASPECT_TRINE, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 0); + const char *text = slurp(f); + + assert(strstr(text, "depression") != NULL); + assert(strstr(text, "past associations") != NULL); + + fclose(f); + printf("PASS test_soft_aspect_adds_harmonious_bonus\n"); +} + +static void test_conjunction_is_neutral_prints_base_only(void) { + Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN, + .type = ASPECT_CONJUNCTION, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 0); + const char *text = slurp(f); + + assert(strstr(text, "depression") != NULL); + assert(strstr(text, "past associations") == NULL); + + fclose(f); + printf("PASS test_conjunction_is_neutral_prints_base_only\n"); +} + +/* The Sun has no unconditional base text (see narrative.c) - a neutral + * conjunction to it should print nothing at all. */ +static void test_empty_base_and_neutral_aspect_prints_nothing(void) { + Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON, + .type = ASPECT_CONJUNCTION, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 0); + long len = ftell(f); + + assert(len == 0); + + fclose(f); + printf("PASS test_empty_base_and_neutral_aspect_prints_nothing\n"); +} + +static void test_discordant_aspect_on_empty_base_planet(void) { + Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON, + .type = ASPECT_OPPOSITION, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 0); + const char *text = slurp(f); + + assert(strstr(text, "Degradation") != NULL); + + fclose(f); + printf("PASS test_discordant_aspect_on_empty_base_planet\n"); +} + +static void test_known_house_adds_area_framing(void) { + Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON, + .type = ASPECT_SQUARE, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 3); + const char *text = slurp(f); + + assert(strstr(text, "house 3") != NULL); + assert(strstr(text, "communication") != NULL); + assert(strstr(text, "depression") != NULL); + + fclose(f); + printf("PASS test_known_house_adds_area_framing\n"); +} + +static void test_unknown_house_omits_area_framing(void) { + Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON, + .type = ASPECT_SQUARE, .orb = 1.0 }; + FILE *f = tmpfile(); + narrative_print(f, " ", &a, 0); /* 0 = "no data" sentinel, not a real house */ + const char *text = slurp(f); + + assert(strstr(text, "house") == NULL); + assert(strstr(text, "depression") != NULL); + + fclose(f); + printf("PASS test_unknown_house_omits_area_framing\n"); +} + +int main(void) { + test_hard_aspect_shows_base_but_not_harmonious_bonus(); + test_soft_aspect_adds_harmonious_bonus(); + test_conjunction_is_neutral_prints_base_only(); + test_empty_base_and_neutral_aspect_prints_nothing(); + test_discordant_aspect_on_empty_base_planet(); + test_known_house_adds_area_framing(); + test_unknown_house_omits_area_framing(); + printf("All narrative tests passed.\n"); + return 0; +} diff --git a/res/Transits_and_Planetary_Periods.pdf b/res/Transits_and_Planetary_Periods.pdf new file mode 100644 index 0000000..bed65a2 Binary files /dev/null and b/res/Transits_and_Planetary_Periods.pdf differ