Compare commits
2 Commits
28558b4343
...
41422adf98
| Author | SHA1 | Date | |
|---|---|---|---|
| 41422adf98 | |||
| 1136e3257a |
@@ -3,6 +3,11 @@
|
|||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# Durable backup of the same real birth data, outside dist/ so a dist/
|
||||||
|
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
||||||
|
# commit this either.
|
||||||
|
/engine/scripts/user.properties.local
|
||||||
|
|
||||||
*.o
|
*.o
|
||||||
core
|
core
|
||||||
core.*
|
core.*
|
||||||
|
|||||||
@@ -45,14 +45,20 @@ dist/deck-engine --seed "2026-07-03" \
|
|||||||
```
|
```
|
||||||
|
|
||||||
`dist/user.properties` is seeded once from `engine/scripts/user.properties.template`
|
`dist/user.properties` is seeded once from `engine/scripts/user.properties.template`
|
||||||
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.) and is **never
|
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.) — `run-engine.sh`/
|
||||||
overwritten by later builds** — `run-engine.sh`/`run-interpreter.sh`
|
`run-interpreter.sh` refuse to run (with a clear error) until the
|
||||||
refuse to run (with a clear error) until the placeholders are replaced
|
placeholders are replaced with real values. Because `dist/` itself is
|
||||||
with real values. `dist/run-interpreter.sh` is the equivalent daily-use
|
disposable (`make clean`, or just deleting the directory, wipes it), the
|
||||||
wrapper for the interpreter: it runs `run-engine.sh`'s same OS-clock
|
`scripts` Makefile target also keeps a durable backup outside `dist/`:
|
||||||
seed/date and `user.properties` birth data through `deck-engine --format
|
once `dist/user.properties` looks filled in (no leftover `YOUR_...`), every
|
||||||
json`, piped straight into `dist/interpreter-cli` — see "Interpretation"
|
build copies it out to `engine/scripts/user.properties.local`
|
||||||
below.
|
(gitignored); if `dist/user.properties` is ever missing when a build
|
||||||
|
runs, it's restored from that backup instead of being reseeded from the
|
||||||
|
placeholder template. `dist/run-interpreter.sh` is the equivalent
|
||||||
|
daily-use wrapper for the interpreter: it runs `run-engine.sh`'s same
|
||||||
|
OS-clock seed/date and `user.properties` birth data through `deck-engine
|
||||||
|
--format json`, piped straight into `dist/interpreter-cli` — see
|
||||||
|
"Interpretation" below.
|
||||||
|
|
||||||
Run a single smoke test by editing `engine/tests/smoke_test.c`'s `main()`
|
Run a single smoke test by editing `engine/tests/smoke_test.c`'s `main()`
|
||||||
temporarily, or just read its assertions — there's no test filter flag,
|
temporarily, or just read its assertions — there's no test filter flag,
|
||||||
@@ -65,7 +71,10 @@ deck_in_a_dash/
|
|||||||
res/ Tarot card art (22 RWS Major Arcana JPEGs),
|
res/ Tarot card art (22 RWS Major Arcana JPEGs),
|
||||||
Waite's "Pictorial Key to the Tarot" (PDF,
|
Waite's "Pictorial Key to the Tarot" (PDF,
|
||||||
public domain — source of all card/spread
|
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/
|
engine/
|
||||||
third_party/astronomy/ Vendored cosinekitty/astronomy C library
|
third_party/astronomy/ Vendored cosinekitty/astronomy C library
|
||||||
(MIT, pinned commit — see VENDORED.md).
|
(MIT, pinned commit — see VENDORED.md).
|
||||||
@@ -193,11 +202,14 @@ into the Pebble watchapp unchanged later. Only `reading_print_text`/
|
|||||||
|
|
||||||
A second, separate top-level module and **its own binary**
|
A second, separate top-level module and **its own binary**
|
||||||
(`dist/interpreter-cli`, sibling to `dist/deck-engine`) that scores *how
|
(`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
|
significant a day's transits are* and, for each of the top items, prints
|
||||||
raw reading into an actual narrative" effort described in project chat
|
a short narrative sentence about what that transit classically means —
|
||||||
history, not yet producing any narrative text. `deck-engine` itself is
|
the first piece of a larger "turn the raw reading into an actual
|
||||||
deliberately unchanged by this beyond gaining `--format json`; the two
|
narrative" effort described in project chat history; the Celtic Cross
|
||||||
binaries compose over that JSON, never by linking together:
|
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
|
```bash
|
||||||
make # -> dist/deck-engine (unchanged; --format json is new) and dist/interpreter-cli
|
make # -> dist/deck-engine (unchanged; --format json is new) and dist/interpreter-cli
|
||||||
@@ -217,11 +229,15 @@ between the two binaries (see the next bullet).
|
|||||||
every aspect in `reading->transits.aspects[]`, keeps the **top 5** by
|
every aspect in `reading->transits.aspects[]`, keeps the **top 5** by
|
||||||
score in `DailyInterpretation.top_items[]` (descending, evicting the
|
score in `DailyInterpretation.top_items[]` (descending, evicting the
|
||||||
rest), and classifies the day into a `DaySignificance` enum (`DAY_QUIET`
|
rest), and classifies the day into a `DaySignificance` enum (`DAY_QUIET`
|
||||||
... `DAY_MAJOR`) from the single highest-scoring item.
|
... `DAY_MAJOR`) from the single highest-scoring item. The enum also
|
||||||
`interpretation_deserves_framing()` is true only at `DAY_MAJOR` — the
|
carries a trailing `DAY_SIGNIFICANCE_COUNT` sentinel (not a real level)
|
||||||
intended hook for giving the Celtic Cross reading a "this matters"
|
purely so callers can print the level as a rank, e.g. `main.c`'s
|
||||||
intro sentence naming `top_items[0]` on days that earn it; that
|
`"%s (%d/%d)"` → `"Notable (2/4)"` — `interp.day_level + 1` out of
|
||||||
rendering doesn't exist yet, `main.c`'s report is plain text only.
|
`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) ×
|
- Score = `transiting-planet weight (slow/outer planets score higher) ×
|
||||||
orb tightness (1.0 = exact, ~0 = at the edge) × natal-luminary bonus
|
orb tightness (1.0 = exact, ~0 = at the edge) × natal-luminary bonus
|
||||||
(transits to natal Sun/Moon score higher than to other planets)`. The
|
(transits to natal Sun/Moon score higher than to other planets)`. The
|
||||||
@@ -229,6 +245,29 @@ between the two binaries (see the next bullet).
|
|||||||
thresholds in `significance.c` are hand-tuned, not derived from
|
thresholds in `significance.c` are hand-tuned, not derived from
|
||||||
anything physical — expect to retune them once real readings are
|
anything physical — expect to retune them once real readings are
|
||||||
compared against how "major" a day actually feels.
|
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:
|
- **`reading_print_json`** (`engine/src/reading.c`) is the wire format:
|
||||||
the full `DailyReading` (natal, transits, spread), using the
|
the full `DailyReading` (natal, transits, spread), using the
|
||||||
language-independent `astro_*_slug()`/`tarot_*_slug()` accessors
|
language-independent `astro_*_slug()`/`tarot_*_slug()` accessors
|
||||||
@@ -241,21 +280,29 @@ between the two binaries (see the next bullet).
|
|||||||
Unlike the core engine, it uses `malloc`; `interpreter-cli` was never
|
Unlike the core engine, it uses `malloc`; `interpreter-cli` was never
|
||||||
meant to run on the watch itself.
|
meant to run on the watch itself.
|
||||||
- **`reading_io.c`** walks the parsed JSON down to `"transits"."aspects"`
|
- **`reading_io.c`** walks the parsed JSON down to `"transits"."aspects"`
|
||||||
and fills a `DailyReading` with just that (everything else is left
|
and fills a `DailyReading` with those (used for scoring), plus
|
||||||
zeroed - `interpret_daily_reading` doesn't read `natal`/`spread`
|
`"natal"."bodies"`/`"transits"."bodies"` (each body's sign + whole-sign
|
||||||
either). An aspect naming a planet/aspect-type slug it doesn't
|
house, via `parse_bodies()` — used only for the sign/house context
|
||||||
recognize, or a document missing `"transits"."aspects"` entirely (as
|
`main.c` prints alongside each significant event, never for scoring).
|
||||||
opposed to a present-but-empty array, which is a valid "no aspects
|
`"spread"` is left zeroed entirely - nothing reads it. An aspect naming
|
||||||
today"), makes the whole load fail rather than silently dropping data.
|
a planet/aspect-type slug it doesn't recognize, or a document missing
|
||||||
|
`"transits"."aspects"` entirely (as opposed to a present-but-empty
|
||||||
|
array, which is a valid "no aspects today"), makes the whole load fail;
|
||||||
|
by contrast an unrecognized body slug or missing `house` inside
|
||||||
|
`"bodies"` is silently skipped (that body's `house` stays `0`, an
|
||||||
|
invalid whole-sign house number used as "no data" - `main.c` checks for
|
||||||
|
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**:
|
- **Deliberately header-only dependency on the engine, throughout**:
|
||||||
`significance.h`/`reading_io.h` `#include engine/src/reading.h` for the
|
`significance.h`/`reading_io.h`/`narrative.h` `#include` engine headers
|
||||||
struct/enum *definitions*, but the root `Makefile` never compiles or
|
(`reading.h`/`astro.h`) for the struct/enum *definitions*, but the root
|
||||||
links any engine `.c` file (or the vendored Astronomy Engine) into the
|
`Makefile` never compiles or links any engine `.c` file (or the
|
||||||
interpreter's binary or tests — every
|
vendored Astronomy Engine) into the interpreter's binary or tests —
|
||||||
|
every
|
||||||
test in `interpreter/tests/` runs against hand-built JSON text or
|
test in `interpreter/tests/` runs against hand-built JSON text or
|
||||||
hand-built `DailyReading` values, with no real ephemeris/tarot-draw
|
hand-built `DailyReading` values, with no real ephemeris/tarot-draw
|
||||||
call involved anywhere. Consequently `reading_io.c` (planet/aspect
|
call involved anywhere. Consequently `reading_io.c` (planet/aspect/sign
|
||||||
slugs) and `main.c` (planet/aspect display names) each duplicate a
|
slugs) and `main.c` (planet/aspect/sign display names) each duplicate a
|
||||||
small local table rather than linking `astro.c` to reuse its own -
|
small local table rather than linking `astro.c` to reuse its own -
|
||||||
keep those in sync if the engine's slugs/names ever change.
|
keep those in sync if the engine's slugs/names ever change.
|
||||||
- Only `Aspect`s compete for the top 5 so far (`SignificantItemKind` is
|
- Only `Aspect`s compete for the top 5 so far (`SignificantItemKind` is
|
||||||
@@ -274,3 +321,7 @@ between the two binaries (see the next bullet).
|
|||||||
- `engine/i18n/de.lang`'s card/spread text is an original translation of
|
- `engine/i18n/de.lang`'s card/spread text is an original translation of
|
||||||
that same public-domain English text, made for this project — not
|
that same public-domain English text, made for this project — not
|
||||||
taken from any specific published German edition.
|
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).
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ ENGINE_I18N_DIR := engine/i18n
|
|||||||
ENGINE_SCRIPTS_DIR := engine/scripts
|
ENGINE_SCRIPTS_DIR := engine/scripts
|
||||||
RES_IMG_DIR := res/img
|
RES_IMG_DIR := res/img
|
||||||
|
|
||||||
|
# Durable copy of the user's real birth data, outside dist/ (which is
|
||||||
|
# disposable - `make clean`, or just deleting the directory, wipes it).
|
||||||
|
# Gitignored: see .gitignore.
|
||||||
|
USER_PROPS_BACKUP := $(ENGINE_SCRIPTS_DIR)/user.properties.local
|
||||||
|
|
||||||
# Engine logic shared by the CLI and the test binary (no main()).
|
# Engine logic shared by the CLI and the test binary (no main()).
|
||||||
ENGINE_SRCS := $(ENGINE_SRC_DIR)/rng.c $(ENGINE_SRC_DIR)/tarot.c $(ENGINE_SRC_DIR)/tarot_data.c \
|
ENGINE_SRCS := $(ENGINE_SRC_DIR)/rng.c $(ENGINE_SRC_DIR)/tarot.c $(ENGINE_SRC_DIR)/tarot_data.c \
|
||||||
$(ENGINE_SRC_DIR)/astro.c $(ENGINE_SRC_DIR)/reading.c $(ENGINE_SRC_DIR)/i18n.c \
|
$(ENGINE_SRC_DIR)/astro.c $(ENGINE_SRC_DIR)/reading.c $(ENGINE_SRC_DIR)/i18n.c \
|
||||||
@@ -41,7 +46,8 @@ ENGINE_TEST_BINARY := $(BUILD_DIR)/smoke_test
|
|||||||
INTERP_SRC_DIR := interpreter/src
|
INTERP_SRC_DIR := interpreter/src
|
||||||
INTERP_TEST_DIR := interpreter/tests
|
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_LIB_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(INTERP_LIB_SRCS))
|
||||||
INTERP_MAIN_OBJ := $(OBJ_DIR)/$(INTERP_SRC_DIR)/main.o
|
INTERP_MAIN_OBJ := $(OBJ_DIR)/$(INTERP_SRC_DIR)/main.o
|
||||||
|
|
||||||
@@ -49,8 +55,10 @@ INTERP_BINARY := $(DIST_DIR)/interpreter-cli
|
|||||||
|
|
||||||
SIGNIFICANCE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/significance_test.o
|
SIGNIFICANCE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/significance_test.o
|
||||||
JSON_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/json_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
|
SIGNIFICANCE_TEST_BIN := $(BUILD_DIR)/significance_test
|
||||||
JSON_TEST_BIN := $(BUILD_DIR)/json_test
|
JSON_TEST_BIN := $(BUILD_DIR)/json_test
|
||||||
|
NARRATIVE_TEST_BIN := $(BUILD_DIR)/narrative_test
|
||||||
|
|
||||||
.PHONY: all test clean images i18n scripts
|
.PHONY: all test clean images i18n scripts
|
||||||
|
|
||||||
@@ -79,21 +87,33 @@ i18n:
|
|||||||
cp $(ENGINE_I18N_DIR)/*.lang $(DIST_I18N_DIR)/
|
cp $(ENGINE_I18N_DIR)/*.lang $(DIST_I18N_DIR)/
|
||||||
|
|
||||||
# run-engine.sh/run-interpreter.sh are refreshed every build.
|
# run-engine.sh/run-interpreter.sh are refreshed every build.
|
||||||
# user.properties is only seeded once (from the placeholder template) so
|
#
|
||||||
# a rebuild never clobbers the user's own filled-in birth data.
|
# user.properties is backed up/restored across dist/ wipes rather than
|
||||||
|
# just "seeded once": if dist/user.properties exists and looks filled in
|
||||||
|
# (no leftover YOUR_... placeholder), it's copied out to
|
||||||
|
# USER_PROPS_BACKUP; otherwise, if that backup exists (e.g. dist/ was
|
||||||
|
# just deleted), it's copied back in; otherwise (genuinely first run)
|
||||||
|
# dist/user.properties is seeded from the placeholder template, same as
|
||||||
|
# before.
|
||||||
scripts:
|
scripts:
|
||||||
@mkdir -p $(DIST_DIR)
|
@mkdir -p $(DIST_DIR)
|
||||||
cp $(ENGINE_SCRIPTS_DIR)/run-engine.sh $(DIST_DIR)/run-engine.sh
|
cp $(ENGINE_SCRIPTS_DIR)/run-engine.sh $(DIST_DIR)/run-engine.sh
|
||||||
chmod +x $(DIST_DIR)/run-engine.sh
|
chmod +x $(DIST_DIR)/run-engine.sh
|
||||||
cp $(ENGINE_SCRIPTS_DIR)/run-interpreter.sh $(DIST_DIR)/run-interpreter.sh
|
cp $(ENGINE_SCRIPTS_DIR)/run-interpreter.sh $(DIST_DIR)/run-interpreter.sh
|
||||||
chmod +x $(DIST_DIR)/run-interpreter.sh
|
chmod +x $(DIST_DIR)/run-interpreter.sh
|
||||||
test -f $(DIST_DIR)/user.properties || \
|
@if [ -f $(DIST_DIR)/user.properties ] && ! grep -q 'YOUR_' $(DIST_DIR)/user.properties; then \
|
||||||
cp $(ENGINE_SCRIPTS_DIR)/user.properties.template $(DIST_DIR)/user.properties
|
cp $(DIST_DIR)/user.properties $(USER_PROPS_BACKUP); \
|
||||||
|
elif [ -f $(USER_PROPS_BACKUP) ]; then \
|
||||||
|
cp $(USER_PROPS_BACKUP) $(DIST_DIR)/user.properties; \
|
||||||
|
elif [ ! -f $(DIST_DIR)/user.properties ]; then \
|
||||||
|
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)
|
./$(ENGINE_TEST_BINARY)
|
||||||
./$(SIGNIFICANCE_TEST_BIN)
|
./$(SIGNIFICANCE_TEST_BIN)
|
||||||
./$(JSON_TEST_BIN)
|
./$(JSON_TEST_BIN)
|
||||||
|
./$(NARRATIVE_TEST_BIN)
|
||||||
|
|
||||||
$(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ)
|
$(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
@@ -104,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)
|
$(JSON_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/json.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/reading_io.o $(JSON_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
|
$(NARRATIVE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/narrative.o $(NARRATIVE_TEST_OBJ)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(OBJ_DIR)/%.o: %.c
|
$(OBJ_DIR)/%.o: %.c
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ dropping a translated `key=value` file into `engine/i18n/` — see
|
|||||||
|
|
||||||
```
|
```
|
||||||
res/ Tarot card art (22 RWS Major Arcana), the
|
res/ Tarot card art (22 RWS Major Arcana), the
|
||||||
Waite PDF, logo, research notes.
|
Waite and Sepharial PDFs, logo, research notes.
|
||||||
engine/
|
engine/
|
||||||
third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT).
|
third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT).
|
||||||
src/ Engine source (rng, tarot, astro, i18n, reading, CLI).
|
src/ Engine source (rng, tarot, astro, i18n, reading, CLI).
|
||||||
@@ -45,7 +45,8 @@ engine/
|
|||||||
the user.properties template.
|
the user.properties template.
|
||||||
dist/ Build output (gitignored) — see below.
|
dist/ Build output (gitignored) — see below.
|
||||||
interpreter/ Separate module + binary (dist/interpreter-cli):
|
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
|
reading deck-engine's --format json output
|
||||||
(see CLAUDE.md).
|
(see CLAUDE.md).
|
||||||
docs/ Architecture/dataflow diagrams, input/output format reference.
|
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.
|
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
|
Waite's *The Pictorial Key to the Tarot* (1911), which is in the public
|
||||||
domain.
|
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).
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ know before changing the engine. This document is the visual overview.
|
|||||||
- **`dist/`** is generated by the single root `Makefile`: the
|
- **`dist/`** is generated by the single root `Makefile`: the
|
||||||
`deck-engine` and `interpreter-cli` binaries, a copy of the card art
|
`deck-engine` and `interpreter-cli` binaries, a copy of the card art
|
||||||
under `img/`, a copy of the translation files under `i18n/`,
|
under `img/`, a copy of the translation files under `i18n/`,
|
||||||
`run-engine.sh`/`run-interpreter.sh`, and a `user.properties` template
|
`run-engine.sh`/`run-interpreter.sh`, and `user.properties`. Since
|
||||||
that's seeded once and never overwritten by later builds.
|
`dist/` itself is disposable, `user.properties` also gets backed up to
|
||||||
|
`engine/scripts/user.properties.local` (gitignored) once it's filled
|
||||||
|
in, and restored from there if `dist/` is ever wiped — see
|
||||||
|
`CLAUDE.md`'s "Build & run".
|
||||||
- **The Pebble watchapp does not exist yet.** When it's built, it will
|
- **The Pebble watchapp does not exist yet.** When it's built, it will
|
||||||
call `reading_generate()` directly and walk the returned struct to lay
|
call `reading_generate()` directly and walk the returned struct to lay
|
||||||
out its own screens — it has no reason to touch `reading_print_*`.
|
out its own screens — it has no reason to touch `reading_print_*`.
|
||||||
|
|||||||
+33
-17
@@ -55,11 +55,15 @@ use:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The file is seeded once from `engine/scripts/user.properties.template`
|
The file is seeded once from `engine/scripts/user.properties.template`
|
||||||
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.) and is **never
|
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.). `run-engine.sh`
|
||||||
overwritten by later builds**. `run-engine.sh` checks every birth field
|
checks every birth field for an empty value or a leftover `YOUR_`
|
||||||
for an empty value or a leftover `YOUR_` placeholder and refuses to run
|
placeholder and refuses to run with a clear error until the file is
|
||||||
with a clear error until the file is filled in; `lang` has no such
|
filled in; `lang` has no such check since it always has a usable
|
||||||
check since it always has a usable default (`en`).
|
default (`en`). Since `dist/` itself can be wiped (`make clean`, or
|
||||||
|
deleting the directory), the build also keeps a durable backup at
|
||||||
|
`engine/scripts/user.properties.local` (gitignored) once the file
|
||||||
|
looks filled in, and restores from it if `dist/user.properties` is
|
||||||
|
ever missing — see `CLAUDE.md`'s "Build & run".
|
||||||
- The optional second argument overrides `lang=` for a single run without
|
- The optional second argument overrides `lang=` for a single run without
|
||||||
editing the file, e.g. `dist/run-engine.sh html de`.
|
editing the file, e.g. `dist/run-engine.sh html de`.
|
||||||
- `dist/run-interpreter.sh` (no arguments) is the equivalent wrapper for
|
- `dist/run-interpreter.sh` (no arguments) is the equivalent wrapper for
|
||||||
@@ -236,22 +240,34 @@ dist/run-interpreter.sh
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **Input**: a file path argument, or stdin if no argument (or `-`) is
|
- **Input**: a file path argument, or stdin if no argument (or `-`) is
|
||||||
given. Only `"transits"."aspects"` is read — `natal` and `spread` may
|
given. `"transits"."aspects"` is required (an empty array is valid and
|
||||||
be present (and are ignored) or omitted entirely, except that
|
means "no notable transits today"; a missing key is treated as invalid
|
||||||
`"transits"."aspects"` itself must be present (an empty array is valid
|
input) and drives scoring. `"natal"."bodies"`/`"transits"."bodies"` are
|
||||||
and means "no notable transits today"; a missing key is treated as
|
read too, for the sign/house context printed alongside each event below
|
||||||
invalid input).
|
(not for scoring) — an entry naming an unrecognized body, or a missing
|
||||||
|
`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
|
- **Output**: a plain-text report — the day's overall significance level
|
||||||
(`Quiet`/`Notable`/`Significant`/`Major`), a "worth a deeper Celtic
|
(`Quiet`/`Notable`/`Significant`/`Major`, plus its rank out of the 4
|
||||||
Cross look" line on `Major` days only, and up to 5 of today's aspects
|
defined levels, e.g. `Notable (2/4)`), a "worth a deeper Celtic Cross
|
||||||
ranked by score:
|
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)
|
(a major transit today - worth a deeper Celtic Cross look)
|
||||||
|
|
||||||
Top 3 significant transits:
|
Top 3 significant transits:
|
||||||
1. Transiting Saturn Square natal Sun (orb 0.5°, score 8.10)
|
1. Transiting Saturn in Aries (house 2) Square natal Sun in Taurus (house 3) (orb 0.5°, score 8.10)
|
||||||
2. Transiting Pluto Opposition natal Moon (orb 0.2°, score 7.92)
|
In matters of money, possessions, and personal values (house 2). Brings depression, stagnation, hindrances and obstacles, and some deprivation of the usual benefits.
|
||||||
3. Transiting Jupiter Trine natal Venus (orb 2.1°, score 3.40)
|
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.
|
||||||
```
|
```
|
||||||
|
|||||||
+79
-13
@@ -75,26 +75,92 @@ spread — the same source most modern tarot decks trace back to.
|
|||||||
Not every day is equally eventful astrologically. A separate,
|
Not every day is equally eventful astrologically. A separate,
|
||||||
optional step can look at today's aspects (part 2, above) and works
|
optional step can look at today's aspects (part 2, above) and works
|
||||||
out an overall **significance level** for the day — Quiet, Notable,
|
out an overall **significance level** for the day — Quiet, Notable,
|
||||||
Significant, or Major — based on which planets are involved, how exact
|
Significant, or Major, shown together with its rank out of those four
|
||||||
the angle is, and whether it touches your Sun or Moon (the two most
|
(for example "Notable (2/4)") — based on which planets are involved,
|
||||||
personally weighted points in a chart). On a genuinely Major day, it
|
how exact the angle is, and whether it touches your Sun or Moon (the
|
||||||
also calls out the single most important thing happening, as a hint
|
two most personally weighted points in a chart). On a genuinely Major
|
||||||
that it's worth paying closer attention to your tarot spread that day
|
day, it also calls out the single most important thing happening, as a
|
||||||
rather than treating it as routine. Quiet days still get a full
|
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
|
reading — this step only adds a "pay attention today" flag, it never
|
||||||
removes anything.
|
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
|
## What a day's reading actually gives you
|
||||||
|
|
||||||
Put together, one day's reading contains: your natal chart (for
|
Put together, one day's reading contains: your natal chart (for
|
||||||
reference), today's sky and how it's speaking to your chart, how
|
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
|
significant today is overall (with a plain-language note on each of the
|
||||||
the day. Right now these sit side by side — the astrology tells you
|
main events driving that), and your 10-card Celtic Cross spread for the
|
||||||
*how eventful* today is and *where* to pay attention, and the tarot
|
day. Right now these sit side by side — the astrology tells you *how
|
||||||
spread gives you the actual guidance to sit with. They don't yet get
|
eventful* today is and *where* to pay attention, and the tarot spread
|
||||||
woven into a single narrative (e.g. a spread explained *in light of*
|
gives you the actual guidance to sit with. They don't yet get woven
|
||||||
today's significant transit) — that combined storytelling is the next
|
into a single narrative (e.g. a spread explained *in light of* today's
|
||||||
piece to build, not something you get today.
|
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
|
*(This page, like that piece, is still a work in progress — expect it
|
||||||
to grow as the reading itself does.)*
|
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).
|
||||||
|
|||||||
+28
-6
@@ -3,6 +3,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "narrative.h"
|
||||||
#include "reading_io.h"
|
#include "reading_io.h"
|
||||||
#include "significance.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"
|
"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"
|
"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"
|
"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"
|
"Typical use:\n"
|
||||||
" dist/deck-engine ... --format json | %s\n"
|
" dist/deck-engine ... --format json | %s\n"
|
||||||
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
||||||
@@ -57,6 +58,22 @@ static const char *aspect_display_name(AspectType type) {
|
|||||||
return names[type];
|
return names[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *sign_display_name(ZodiacSign sign) {
|
||||||
|
static const char *const names[12] = {
|
||||||
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||||
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||||
|
};
|
||||||
|
return names[sign];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pos->house is 0 when reading_load_json had no sign/house data for this
|
||||||
|
* body (see parse_bodies() in reading_io.c) - a valid whole-sign house
|
||||||
|
* is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
|
||||||
|
static void print_body_in_sign(const PlanetPosition *pos) {
|
||||||
|
if (pos->house < 1 || pos->house > 12) return;
|
||||||
|
printf(" in %s (house %d)", sign_display_name(pos->sign), pos->house);
|
||||||
|
}
|
||||||
|
|
||||||
static const char *day_level_name(DaySignificance level) {
|
static const char *day_level_name(DaySignificance level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case DAY_QUIET: return "Quiet";
|
case DAY_QUIET: return "Quiet";
|
||||||
@@ -99,7 +116,8 @@ int main(int argc, char **argv) {
|
|||||||
DailyInterpretation interp;
|
DailyInterpretation interp;
|
||||||
interpret_daily_reading(&reading, &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)) {
|
if (interpretation_deserves_framing(&interp)) {
|
||||||
printf("(a major transit today - worth a deeper Celtic Cross look)\n");
|
printf("(a major transit today - worth a deeper Celtic Cross look)\n");
|
||||||
}
|
}
|
||||||
@@ -114,10 +132,14 @@ int main(int argc, char **argv) {
|
|||||||
interp.top_item_count == 1 ? "" : "s");
|
interp.top_item_count == 1 ? "" : "s");
|
||||||
for (int i = 0; i < interp.top_item_count; i++) {
|
for (int i = 0; i < interp.top_item_count; i++) {
|
||||||
const SignificantItem *item = &interp.top_items[i];
|
const SignificantItem *item = &interp.top_items[i];
|
||||||
printf(" %d. Transiting %s %s natal %s (orb %.1f\xc2\xb0, score %.2f)\n", i + 1,
|
const Aspect *a = &item->aspect;
|
||||||
body_display_name(item->aspect.transiting_planet),
|
|
||||||
aspect_display_name(item->aspect.type),
|
printf(" %d. Transiting %s", i + 1, body_display_name(a->transiting_planet));
|
||||||
body_display_name(item->aspect.natal_planet), item->aspect.orb, item->score);
|
print_body_in_sign(&reading.transits.bodies[a->transiting_planet]);
|
||||||
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#include "narrative.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#ifndef DECK_NARRATIVE_H
|
||||||
|
#define DECK_NARRATIVE_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* 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
|
||||||
@@ -17,6 +17,11 @@ static const char *const k_aspect_slug[5] = {
|
|||||||
"conjunction", "sextile", "square", "trine", "opposition",
|
"conjunction", "sextile", "square", "trine", "opposition",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const char *const k_sign_slug[12] = {
|
||||||
|
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
||||||
|
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||||
|
};
|
||||||
|
|
||||||
static bool body_from_slug(const char *slug, Body *out) {
|
static bool body_from_slug(const char *slug, Body *out) {
|
||||||
for (int i = 0; i < NUM_BODIES; i++) {
|
for (int i = 0; i < NUM_BODIES; i++) {
|
||||||
if (strcmp(slug, k_body_slug[i]) == 0) {
|
if (strcmp(slug, k_body_slug[i]) == 0) {
|
||||||
@@ -27,6 +32,41 @@ static bool body_from_slug(const char *slug, Body *out) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool sign_from_slug(const char *slug, ZodiacSign *out) {
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
if (strcmp(slug, k_sign_slug[i]) == 0) {
|
||||||
|
*out = (ZodiacSign)i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fills out[NUM_BODIES] from a "bodies" JSON array, the shape shared by
|
||||||
|
* both "natal" and "transits" (see reading_print_json in engine/src/
|
||||||
|
* reading.c). Used only for sign/house context in significant-event
|
||||||
|
* reporting, not for scoring, so this is deliberately lenient: an entry
|
||||||
|
* with an unrecognized/missing "body" slug is skipped, and a missing
|
||||||
|
* "house" leaves that body's house at 0 - not a valid whole-sign house
|
||||||
|
* number (always 1-12), so callers use house == 0 as "no position data
|
||||||
|
* available" rather than this failing the whole load. */
|
||||||
|
static void parse_bodies(const JsonValue *bodies, PlanetPosition out[NUM_BODIES]) {
|
||||||
|
int count = json_array_count(bodies);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
const JsonValue *item = json_array_get(bodies, i);
|
||||||
|
Body body;
|
||||||
|
if (!body_from_slug(json_as_string(json_object_get(item, "body")), &body)) continue;
|
||||||
|
|
||||||
|
ZodiacSign sign;
|
||||||
|
if (sign_from_slug(json_as_string(json_object_get(item, "sign")), &sign)) {
|
||||||
|
out[body].sign = sign;
|
||||||
|
}
|
||||||
|
out[body].degree_in_sign = json_as_number(json_object_get(item, "degree_in_sign"));
|
||||||
|
out[body].ecliptic_longitude = json_as_number(json_object_get(item, "ecliptic_longitude"));
|
||||||
|
out[body].house = (int)json_as_number(json_object_get(item, "house"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static bool aspect_type_from_slug(const char *slug, AspectType *out) {
|
static bool aspect_type_from_slug(const char *slug, AspectType *out) {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
if (strcmp(slug, k_aspect_slug[i]) == 0) {
|
if (strcmp(slug, k_aspect_slug[i]) == 0) {
|
||||||
@@ -78,6 +118,10 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
|||||||
}
|
}
|
||||||
out->transits.aspect_count = filled;
|
out->transits.aspect_count = filled;
|
||||||
|
|
||||||
|
const JsonValue *natal = json_object_get(root, "natal");
|
||||||
|
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||||
|
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
||||||
|
|
||||||
json_free(root);
|
json_free(root);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,18 @@
|
|||||||
|
|
||||||
/* Parses deck-engine's `--format json` output (text, null-terminated at
|
/* Parses deck-engine's `--format json` output (text, null-terminated at
|
||||||
* text[length]) and fills *out with just enough of a DailyReading for
|
* text[length]) and fills *out with just enough of a DailyReading for
|
||||||
* interpret_daily_reading() to work on: out->transits.aspects[]/
|
* interpret_daily_reading() to work on and for reporting sign/house
|
||||||
* aspect_count. out->natal and out->spread are left zeroed - nothing
|
* context around each significant event: out->transits.aspects[]/
|
||||||
* here reads them (see significance.c), so there's no reason to parse
|
* aspect_count (used for scoring - see significance.c), and
|
||||||
* the rest of the document yet.
|
* out->natal.bodies[]/out->transits.bodies[] (sign + house per body,
|
||||||
|
* used only for display). out->natal.ascendant_longitude/houses[] and
|
||||||
|
* out->spread are left zeroed - nothing reads them yet.
|
||||||
*
|
*
|
||||||
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
||||||
* present as an array (an empty array is fine - that's a real "no
|
* present as an array (an empty array is fine - that's a real "no
|
||||||
* aspects today" reading, not an error). */
|
* aspects today" reading, not an error). A missing/unrecognized entry
|
||||||
|
* in "natal"."bodies"/"transits"."bodies" is not an error - see
|
||||||
|
* parse_bodies() in reading_io.c. */
|
||||||
bool reading_load_json(const char *text, size_t length, DailyReading *out);
|
bool reading_load_json(const char *text, size_t length, DailyReading *out);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ typedef enum {
|
|||||||
DAY_QUIET = 0,
|
DAY_QUIET = 0,
|
||||||
DAY_NOTABLE,
|
DAY_NOTABLE,
|
||||||
DAY_SIGNIFICANT,
|
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;
|
} DaySignificance;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -41,14 +41,16 @@ static void test_json_parse_rejects_malformed(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* A trimmed-but-structurally-faithful fixture matching deck-engine's real
|
/* A trimmed-but-structurally-faithful fixture matching deck-engine's real
|
||||||
* --format json shape: natal/spread sections are present with decoy
|
* --format json shape: "spread" is present with decoy content the loader
|
||||||
* content the loader must skip over without understanding their schema,
|
* must skip over without understanding its schema, to prove it navigates
|
||||||
* to prove it navigates by key path rather than assuming any position. */
|
* by key path rather than assuming any position. "natal"/"transits"
|
||||||
|
* bodies are real (if partial) - reading_load_json now parses sign/house
|
||||||
|
* from them for display, alongside the aspects used for scoring. */
|
||||||
static const char *k_fixture =
|
static const char *k_fixture =
|
||||||
"{"
|
"{"
|
||||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\"}], \"houses\": []},"
|
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
||||||
" \"transits\": {"
|
" \"transits\": {"
|
||||||
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\"}],"
|
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
|
||||||
" \"moon_phase\": \"full\","
|
" \"moon_phase\": \"full\","
|
||||||
" \"aspects\": ["
|
" \"aspects\": ["
|
||||||
" {\"transiting_planet\": \"pluto\", \"natal_planet\": \"moon\", \"type\": \"opposition\", \"orb\": 0.2},"
|
" {\"transiting_planet\": \"pluto\", \"natal_planet\": \"moon\", \"type\": \"opposition\", \"orb\": 0.2},"
|
||||||
@@ -75,6 +77,37 @@ static void test_reading_load_json_extracts_aspects(void) {
|
|||||||
printf("PASS test_reading_load_json_extracts_aspects\n");
|
printf("PASS test_reading_load_json_extracts_aspects\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.natal.bodies[PLANET_SUN].sign == SIGN_TAURUS);
|
||||||
|
assert(reading.natal.bodies[PLANET_SUN].house == 3);
|
||||||
|
assert(reading.transits.bodies[PLANET_SUN].sign == SIGN_CANCER);
|
||||||
|
assert(reading.transits.bodies[PLANET_SUN].house == 5);
|
||||||
|
|
||||||
|
/* Bodies missing from the fixture (e.g. the Moon) get the "no data"
|
||||||
|
* sentinel rather than a garbage house number. */
|
||||||
|
assert(reading.natal.bodies[PLANET_MOON].house == 0);
|
||||||
|
assert(reading.transits.bodies[PLANET_MOON].house == 0);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_extracts_body_sign_and_house\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_unknown_body_slug_is_skipped_not_fatal(void) {
|
||||||
|
const char *text =
|
||||||
|
"{\"natal\": {\"bodies\": [{\"body\": \"xenu\", \"sign\": \"taurus\", \"house\": 3}]},"
|
||||||
|
" \"transits\": {\"aspects\": []}}";
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.transits.aspect_count == 0);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_unknown_body_slug_is_skipped_not_fatal\n");
|
||||||
|
}
|
||||||
|
|
||||||
static void test_reading_load_json_empty_aspects_is_valid(void) {
|
static void test_reading_load_json_empty_aspects_is_valid(void) {
|
||||||
const char *text = "{\"transits\": {\"aspects\": []}}";
|
const char *text = "{\"transits\": {\"aspects\": []}}";
|
||||||
DailyReading reading;
|
DailyReading reading;
|
||||||
@@ -113,6 +146,8 @@ int main(void) {
|
|||||||
test_json_parse_basic_shapes();
|
test_json_parse_basic_shapes();
|
||||||
test_json_parse_rejects_malformed();
|
test_json_parse_rejects_malformed();
|
||||||
test_reading_load_json_extracts_aspects();
|
test_reading_load_json_extracts_aspects();
|
||||||
|
test_reading_load_json_extracts_body_sign_and_house();
|
||||||
|
test_reading_load_json_unknown_body_slug_is_skipped_not_fatal();
|
||||||
test_reading_load_json_empty_aspects_is_valid();
|
test_reading_load_json_empty_aspects_is_valid();
|
||||||
test_reading_load_json_rejects_missing_transits();
|
test_reading_load_json_rejects_missing_transits();
|
||||||
test_reading_load_json_rejects_unknown_slug();
|
test_reading_load_json_rejects_unknown_slug();
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user