1662d550c7
- guidance.c now prints last, after the full spread; both it and main.c's new Celtic Cross readout pull real Waite card meanings via tarot_data.c instead of just naming cards. - interpreter-cli gains --format text|html|json and --lang en|de, matching deck-engine - required linking engine/src/tarot_data.c and i18n.c into the interpreter binary (a narrow, documented exception to its earlier "no engine .c files" policy) and writing German translations for narrative.c's/guidance.c's own text.
187 lines
6.7 KiB
C
187 lines
6.7 KiB
C
#include "reading_io.h"
|
|
#include "json.h"
|
|
|
|
#include <string.h>
|
|
|
|
/* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key
|
|
* tables), duplicated here rather than linking astro.c into this
|
|
* otherwise header-only, independently testable module - same policy
|
|
* and same reasoning as significance.c's max_orb_for_pair(). Keep in
|
|
* sync if those slugs ever change. */
|
|
static const char *const k_body_slug[NUM_BODIES] = {
|
|
"sun", "moon", "mercury", "venus", "mars",
|
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
};
|
|
|
|
static const char *const k_aspect_slug[5] = {
|
|
"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",
|
|
};
|
|
|
|
/* Mirrors tarot_data.c's own k_card_slug/k_position_slug, same
|
|
* duplication policy as k_body_slug above. */
|
|
static const char *const k_card_slug[TAROT_DECK_SIZE] = {
|
|
"fool", "magician", "high_priestess", "empress", "emperor", "hierophant",
|
|
"lovers", "chariot", "strength", "hermit", "wheel_of_fortune", "justice",
|
|
"hanged_man", "death", "temperance", "devil", "tower", "star", "moon",
|
|
"sun", "judgement", "world",
|
|
};
|
|
|
|
static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
|
|
"present", "challenge", "crown", "foundation", "recent_past",
|
|
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
|
|
};
|
|
|
|
static bool body_from_slug(const char *slug, Body *out) {
|
|
for (int i = 0; i < NUM_BODIES; i++) {
|
|
if (strcmp(slug, k_body_slug[i]) == 0) {
|
|
*out = (Body)i;
|
|
return true;
|
|
}
|
|
}
|
|
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) {
|
|
for (int i = 0; i < 5; i++) {
|
|
if (strcmp(slug, k_aspect_slug[i]) == 0) {
|
|
*out = (AspectType)i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool card_from_slug(const char *slug, TarotCard *out) {
|
|
for (int i = 0; i < TAROT_DECK_SIZE; i++) {
|
|
if (strcmp(slug, k_card_slug[i]) == 0) {
|
|
*out = (TarotCard)i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
|
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
|
if (strcmp(slug, k_position_slug[i]) == 0) {
|
|
*out = (CelticCrossPosition)i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Fills out->positions[] from a "spread"."positions" JSON array. Used only
|
|
* for guidance_print()'s Attitude/Outcome framing, never for scoring, so
|
|
* this is deliberately lenient like parse_bodies(): an entry with an
|
|
* unrecognized/missing "position" or "card" slug is skipped, and a
|
|
* missing "spread" key entirely just leaves every position zeroed
|
|
* (card = the first enum value, reversed = false) rather than failing
|
|
* the whole load. */
|
|
static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
|
|
int count = json_array_count(positions);
|
|
for (int i = 0; i < count; i++) {
|
|
const JsonValue *item = json_array_get(positions, i);
|
|
CelticCrossPosition position;
|
|
TarotCard card;
|
|
if (!position_from_slug(json_as_string(json_object_get(item, "position")), &position)) continue;
|
|
if (!card_from_slug(json_as_string(json_object_get(item, "card")), &card)) continue;
|
|
|
|
out->positions[position].card = card;
|
|
const JsonValue *reversed = json_object_get(item, "reversed");
|
|
out->positions[position].reversed = reversed && reversed->type == JSON_BOOL && reversed->as.boolean;
|
|
}
|
|
}
|
|
|
|
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
JsonValue *root = json_parse(text, length);
|
|
if (!root) return false;
|
|
|
|
const JsonValue *transits = json_object_get(root, "transits");
|
|
const JsonValue *aspects = transits ? json_object_get(transits, "aspects") : NULL;
|
|
if (!aspects) {
|
|
json_free(root);
|
|
return false;
|
|
}
|
|
|
|
bool ok = true;
|
|
int filled = 0;
|
|
int count = json_array_count(aspects);
|
|
for (int i = 0; i < count && filled < MAX_ASPECTS; i++) {
|
|
const JsonValue *item = json_array_get(aspects, i);
|
|
const char *transiting_slug = json_as_string(json_object_get(item, "transiting_planet"));
|
|
const char *natal_slug = json_as_string(json_object_get(item, "natal_planet"));
|
|
const char *type_slug = json_as_string(json_object_get(item, "type"));
|
|
const JsonValue *orb_value = json_object_get(item, "orb");
|
|
|
|
Body transiting, natal;
|
|
AspectType type;
|
|
if (!body_from_slug(transiting_slug, &transiting) ||
|
|
!body_from_slug(natal_slug, &natal) ||
|
|
!aspect_type_from_slug(type_slug, &type) ||
|
|
!orb_value) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
out->transits.aspects[filled].transiting_planet = transiting;
|
|
out->transits.aspects[filled].natal_planet = natal;
|
|
out->transits.aspects[filled].type = type;
|
|
out->transits.aspects[filled].orb = json_as_number(orb_value);
|
|
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);
|
|
|
|
const JsonValue *spread = json_object_get(root, "spread");
|
|
parse_spread(spread ? json_object_get(spread, "positions") : NULL, &out->spread);
|
|
|
|
json_free(root);
|
|
return ok;
|
|
}
|