Give the interpreter real Celtic Cross meanings and --format/--lang support

- 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.
This commit is contained in:
ml
2026-07-11 19:39:30 +02:00
parent 41422adf98
commit 1662d550c7
23 changed files with 1894 additions and 296 deletions
+59
View File
@@ -22,6 +22,20 @@ static const char *const k_sign_slug[12] = {
"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) {
@@ -77,6 +91,48 @@ static bool aspect_type_from_slug(const char *slug, AspectType *out) {
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));
@@ -122,6 +178,9 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
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;
}