Add core engine: tarot + astrology reading generator with CLI and i18n

Plain-C, dependency-free engine that produces a personalized daily
Celtic Cross tarot spread and natal-chart/transit astrology reading,
plus a standalone CLI (dist/deck-engine) and run.sh wrapper. Includes
English/German output via a simple key=value translation format, and
docs/diagrams describing the architecture and I/O.
This commit is contained in:
ml
2026-07-04 06:49:18 +02:00
commit e4ea31270f
59 changed files with 17134 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
#include "reading.h"
#include "i18n.h"
#include <math.h>
/* UI-label keys ("ui.*") are looked up here rather than in i18n.h/.c
* because they're specific to these two renderers, not part of the
* portable astro/tarot vocabulary. */
static const char *ui(const char *key, const char *fallback) {
return i18n_get(key, fallback);
}
void reading_generate(const char *tarot_seed, const BirthData *birth,
time_t utc_moment, DailyReading *out) {
astro_compute_natal_chart(birth, &out->natal);
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
tarot_draw_celtic_cross(tarot_seed, &out->spread);
}
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));
}
void reading_print_text(const DailyReading *r, FILE *out) {
fprintf(out, "===== %s =====\n", ui("ui.natal_chart", "Natal Chart"));
for (int b = 0; b < NUM_BODIES; b++) {
fprintf(out, "%-8s ", astro_body_name((Body)b));
print_position_deg(out, &r->natal.bodies[b]);
fprintf(out, "\n");
}
fprintf(out, "%s %5.1f%s %s\n", ui("ui.ascendant", "Ascendant"),
fmod(r->natal.ascendant_longitude, 30.0), "\xc2\xb0",
astro_sign_name(r->natal.houses[0]));
fprintf(out, "\n");
fprintf(out, "===== %s =====\n", ui("ui.todays_sky", "Today's Sky"));
fprintf(out, "%s %s\n", ui("ui.moon_phase", "Moon phase:"),
astro_moon_phase_name(r->transits.moon_phase));
for (int b = 0; b < NUM_BODIES; b++) {
fprintf(out, "%-8s ", astro_body_name((Body)b));
print_position_deg(out, &r->transits.bodies[b]);
fprintf(out, "\n");
}
fprintf(out, "\n");
fprintf(out, "===== %s =====\n", ui("ui.todays_aspects", "Today's Aspects to Natal Chart"));
if (r->transits.aspect_count == 0) {
fprintf(out, "%s\n", ui("ui.no_aspects", "(none within orb)"));
}
for (int i = 0; i < r->transits.aspect_count; i++) {
const Aspect *a = &r->transits.aspects[i];
fprintf(out, "%s %s %s %s %s (%s %.1f%s)\n", ui("ui.transiting", "Transiting"),
astro_body_name(a->transiting_planet), astro_aspect_name(a->type),
ui("ui.natal", "natal"), astro_body_name(a->natal_planet),
ui("ui.orb", "orb"), a->orb, "\xc2\xb0");
}
fprintf(out, "\n");
fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celtic Cross"));
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
const TarotDraw *draw = &r->spread.positions[i];
fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticCrossPosition)i),
tarot_card_name(draw->card), draw->reversed ? " " : "",
draw->reversed ? ui("ui.reversed", "(Reversed)") : "");
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
}
}
static void print_body_row_html(FILE *out, const char *label, const PlanetPosition *p) {
fprintf(out, "<tr><td>%s</td><td>%.1f&deg; %s</td></tr>\n", label,
p->degree_in_sign, astro_sign_name(p->sign));
}
void reading_print_html(const DailyReading *r, FILE *out) {
fprintf(out,
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
"<title>%s</title>\n"
"<style>\n"
"body { font-family: sans-serif; max-width: 900px; margin: 2em auto; }\n"
"table { border-collapse: collapse; margin-bottom: 1.5em; }\n"
"td { padding: 2px 10px 2px 0; }\n"
"h1, h2 { border-bottom: 1px solid #ccc; }\n"
".spread { display: flex; flex-wrap: wrap; gap: 1.5em; }\n"
".card { width: 160px; }\n"
".card img { width: 140px; display: block; }\n"
".card img.reversed { transform: rotate(180deg); }\n"
".card .position { font-weight: bold; }\n"
".card .name { font-style: italic; }\n"
"</style></head><body>\n",
ui("ui.page_title", "Deck in a Dash - Daily Reading"));
fprintf(out, "<h1>%s</h1>\n", ui("ui.daily_reading", "Daily Reading"));
fprintf(out, "<h2>%s</h2>\n<table>\n", ui("ui.natal_chart", "Natal Chart"));
for (int b = 0; b < NUM_BODIES; b++) {
print_body_row_html(out, astro_body_name((Body)b), &r->natal.bodies[b]);
}
fprintf(out, "<tr><td>%s</td><td>%.1f&deg; %s</td></tr>\n", ui("ui.ascendant", "Ascendant"),
fmod(r->natal.ascendant_longitude, 30.0), astro_sign_name(r->natal.houses[0]));
fprintf(out, "</table>\n");
fprintf(out, "<h2>%s</h2>\n<p>%s %s</p>\n<table>\n", ui("ui.todays_sky", "Today's Sky"),
ui("ui.moon_phase", "Moon phase:"), astro_moon_phase_name(r->transits.moon_phase));
for (int b = 0; b < NUM_BODIES; b++) {
print_body_row_html(out, astro_body_name((Body)b), &r->transits.bodies[b]);
}
fprintf(out, "</table>\n");
fprintf(out, "<h2>%s</h2>\n<table>\n", ui("ui.todays_aspects", "Today's Aspects to Natal Chart"));
for (int i = 0; i < r->transits.aspect_count; i++) {
const Aspect *a = &r->transits.aspects[i];
fprintf(out, "<tr><td>%s %s</td><td>%s</td><td>%s %s</td><td>%s %.1f&deg;</td></tr>\n",
ui("ui.transiting", "Transiting"), astro_body_name(a->transiting_planet),
astro_aspect_name(a->type), ui("ui.natal", "natal"),
astro_body_name(a->natal_planet), ui("ui.orb", "orb"), a->orb);
}
fprintf(out, "</table>\n");
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
const TarotDraw *draw = &r->spread.positions[i];
fprintf(out,
"<div class=\"card\">\n"
" <div class=\"position\">%s</div>\n"
/* Expects card art at img/<file> next to the HTML file itself -
* `make images` copies res/img/ there alongside the binary in
* dist/. */
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
" <div class=\"name\">%s%s%s</div>\n"
" <div class=\"desc\">%s</div>\n"
" <div class=\"meaning\">%s</div>\n"
"</div>\n",
tarot_position_name((CelticCrossPosition)i),
draw->reversed ? "reversed" : "",
tarot_card_image_file(draw->card), tarot_card_name(draw->card),
tarot_card_name(draw->card), draw->reversed ? " " : "",
draw->reversed ? ui("ui.reversed", "(Reversed)") : "",
tarot_position_description((CelticCrossPosition)i),
tarot_card_meaning(draw->card, draw->reversed));
}
fprintf(out, "</div>\n</body></html>\n");
}