diff --git a/CLAUDE.md b/CLAUDE.md index 8d24a64..41090b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,6 +120,13 @@ you touch this function, keep the intermediate type 64-bit. Engine has no Ascendant function; it's computed directly in `astro.c` from sidereal time + a standard low-precision obliquity polynomial + the RAMC/obliquity/latitude identity (Duffett-Smith & Zwart). +- Every `PlanetPosition` (`astro.h`) carries a whole-sign `house` (1-12), + via `assign_houses()`/`whole_sign_house()`. **`DailyTransits.bodies[*].house` + is always relative to the *natal* Ascendant**, not a fresh "houses for + right now" chart — that's the standard, personalized way transits are + read (e.g. "transiting Jupiter is in your 5th house"), and it's why + `astro_compute_daily_transits` takes the natal chart as a parameter in + the first place (also used for aspect detection). - Aspects (conjunction/sextile/square/trine/opposition) use an 8° orb when a luminary (Sun/Moon) is involved, 6° otherwise. diff --git a/README.md b/README.md index 7592be7..12dc4f1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Deck in a Dash logo + Deck in a Dash logo

# Deck in a Dash diff --git a/docs/input-output-format.md b/docs/input-output-format.md index 1afc987..74e233e 100644 --- a/docs/input-output-format.md +++ b/docs/input-output-format.md @@ -91,10 +91,13 @@ typedef struct { ``` - **`NatalChart`** (`astro.h`): `bodies[10]` (Sun..Pluto, each a sign + - degree-in-sign + raw ecliptic longitude), `ascendant_longitude`, - `houses[12]` (whole-sign: `houses[0]` is the Ascendant's sign). + degree-in-sign + raw ecliptic longitude + whole-sign `house` 1-12), + `ascendant_longitude`, `houses[12]` (whole-sign: `houses[0]` is the + Ascendant's sign). - **`DailyTransits`** (`astro.h`): `bodies[10]` (today's positions, same - shape as above), `moon_phase` (one of 8 named phases), `aspects[]` + + shape as above — `house` here is the natal chart's house the + transiting planet currently occupies, not a fresh "houses for right + now" chart), `moon_phase` (one of 8 named phases), `aspects[]` + `aspect_count` (each aspect names a transiting planet, a natal planet, an `AspectType` — conjunction/sextile/square/trine/opposition — and the orb in degrees). @@ -107,22 +110,22 @@ typedef struct { ### `--format text` Four `=====`-delimited sections, in order: natal chart (each body's sign -+ degree, then the Ascendant), today's sky (Moon phase, then each body's -transiting position), today's aspects to the natal chart (one line per -aspect, with orb), and the Celtic Cross (one block per position: name, -card, `(Reversed)` if applicable, the position's meaning, then the -card's meaning). ++ degree + house, then the Ascendant), today's sky (Moon phase, then each +body's transiting position + the natal house it currently occupies), +today's aspects to the natal chart (one line per aspect, with orb), and +the Celtic Cross (one block per position: name, card, `(Reversed)` if +applicable, the position's meaning, then the card's meaning). ``` ===== Natal Chart ===== -Sun 23.5° Taurus -Moon 15.2° Capricorn +Sun 23.5° Taurus (House 3) +Moon 15.2° Capricorn (House 11) ... Ascendant 18.4° Pisces ===== Today's Sky ===== Moon phase: Waning Gibbous -Sun 11.5° Cancer +Sun 11.5° Cancer (House 5) ... ===== Today's Aspects to Natal Chart ===== diff --git a/engine/i18n/de.lang b/engine/i18n/de.lang index 5f45a68..7e3ce7c 100644 --- a/engine/i18n/de.lang +++ b/engine/i18n/de.lang @@ -14,6 +14,7 @@ ui.todays_sky=Heutiger Himmel ui.todays_aspects=Heutige Aspekte zum Geburtshoroskop ui.celtic_cross=Keltisches Kreuz ui.ascendant=Aszendent +ui.house=Haus ui.moon_phase=Mondphase: ui.no_aspects=(keine im Orbis) ui.reversed=(Umgekehrt) diff --git a/engine/i18n/en.lang b/engine/i18n/en.lang index 78dfc27..9797c94 100644 --- a/engine/i18n/en.lang +++ b/engine/i18n/en.lang @@ -21,6 +21,7 @@ ui.todays_sky=Today's Sky ui.todays_aspects=Today's Aspects to Natal Chart ui.celtic_cross=Celtic Cross ui.ascendant=Ascendant +ui.house=House ui.moon_phase=Moon phase: ui.no_aspects=(none within orb) ui.reversed=(Reversed) diff --git a/engine/src/astro.c b/engine/src/astro.c index 75c9760..e9f8e62 100644 --- a/engine/src/astro.c +++ b/engine/src/astro.c @@ -86,6 +86,20 @@ static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BOD } } +/* Whole-sign house number (1-12) for `sign`, counting from `ascendant_sign` + * as house 1. Used both for the natal chart's own placements and (with the + * natal ascendant) for today's transiting planets - see the `house` field + * comment in astro.h for why transits use the natal houses, not their own. */ +static int whole_sign_house(ZodiacSign sign, ZodiacSign ascendant_sign) { + return ((int)sign - (int)ascendant_sign + 12) % 12 + 1; +} + +static void assign_houses(PlanetPosition bodies[NUM_BODIES], ZodiacSign ascendant_sign) { + for (int b = 0; b < NUM_BODIES; b++) { + bodies[b].house = whole_sign_house(bodies[b].sign, ascendant_sign); + } +} + void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) { astro_time_t time = time_from_birth(birth); @@ -96,6 +110,8 @@ void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) { for (int house = 0; house < 12; house++) { out->houses[house] = (ZodiacSign)((ascendant_sign + house) % 12); } + + assign_houses(out->bodies, out->houses[0]); } static MoonPhaseName moon_phase_from_angle(double angle_deg) { @@ -129,6 +145,7 @@ void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal, astro_time_t time = time_from_unix(utc_moment); compute_body_positions(time, out->bodies); + assign_houses(out->bodies, natal->houses[0]); astro_angle_result_t phase = Astronomy_MoonPhase(time); out->moon_phase = moon_phase_from_angle(phase.angle); diff --git a/engine/src/astro.h b/engine/src/astro.h index 05d1130..14c88df 100644 --- a/engine/src/astro.h +++ b/engine/src/astro.h @@ -60,6 +60,11 @@ typedef struct { double ecliptic_longitude; /* geocentric, degrees, [0, 360) */ ZodiacSign sign; double degree_in_sign; /* [0, 30) */ + int house; /* 1-12, whole-sign, always relative to the natal Ascendant - + * for DailyTransits.bodies this places today's transiting + * planets in the natal chart's own houses (the standard, + * personalized way to read transits), not a fresh "houses + * for right now" chart. */ } PlanetPosition; typedef struct { diff --git a/engine/src/reading.c b/engine/src/reading.c index 08888c9..21db421 100644 --- a/engine/src/reading.c +++ b/engine/src/reading.c @@ -18,7 +18,8 @@ void reading_generate(const char *tarot_seed, const BirthData *birth, } static void print_position_deg(FILE *out, const PlanetPosition *p) { - fprintf(out, "%5.1f%s %s", p->degree_in_sign, "\xc2\xb0", astro_sign_name(p->sign)); + fprintf(out, "%5.1f%s %-11s (%s %d)", p->degree_in_sign, "\xc2\xb0", + astro_sign_name(p->sign), ui("ui.house", "House"), p->house); } void reading_print_text(const DailyReading *r, FILE *out) { @@ -68,8 +69,8 @@ void reading_print_text(const DailyReading *r, FILE *out) { } static void print_body_row_html(FILE *out, const char *label, const PlanetPosition *p) { - fprintf(out, "%s%.1f° %s\n", label, - p->degree_in_sign, astro_sign_name(p->sign)); + fprintf(out, "%s%.1f° %s%s %d\n", label, + p->degree_in_sign, astro_sign_name(p->sign), ui("ui.house", "House"), p->house); } void reading_print_html(const DailyReading *r, FILE *out) { diff --git a/engine/tests/smoke_test.c b/engine/tests/smoke_test.c index 4b852f7..7bdf339 100644 --- a/engine/tests/smoke_test.c +++ b/engine/tests/smoke_test.c @@ -52,6 +52,14 @@ static void test_natal_sun_sign(void) { astro_compute_natal_chart(&birth, &chart); assert(chart.bodies[PLANET_SUN].sign == SIGN_GEMINI); + + /* Whole-sign house invariant: body.house (1-12) must name the house + * whose sign (chart.houses[house-1]) is that same body's sign. */ + for (int b = 0; b < NUM_BODIES; b++) { + assert(chart.bodies[b].house >= 1 && chart.bodies[b].house <= 12); + assert(chart.houses[chart.bodies[b].house - 1] == chart.bodies[b].sign); + } + printf("PASS test_natal_sun_sign\n"); } @@ -74,6 +82,12 @@ static void test_reading_generate_smoke(void) { for (int b = 0; b < NUM_BODIES; b++) { assert(r1.natal.bodies[b].ecliptic_longitude == r2.natal.bodies[b].ecliptic_longitude); assert(r1.transits.bodies[b].ecliptic_longitude == r2.transits.bodies[b].ecliptic_longitude); + + /* Transiting planets' houses are read against the natal chart's + * houses, not a fresh chart for "now" - same invariant as the natal + * bodies, but checked against r1.natal.houses. */ + assert(r1.transits.bodies[b].house >= 1 && r1.transits.bodies[b].house <= 12); + assert(r1.natal.houses[r1.transits.bodies[b].house - 1] == r1.transits.bodies[b].sign); } assert(r1.transits.aspect_count == r2.transits.aspect_count); diff --git a/res/House (astrology) - Wikipedia.pdf b/res/House (astrology) - Wikipedia.pdf new file mode 100644 index 0000000..6c9fada Binary files /dev/null and b/res/House (astrology) - Wikipedia.pdf differ