Introduces watch/, a real Pebble watchapp (built via pebble build) that
build / build (push) Successful in 30s
build / build (push) Successful in 30s
shows the daily tarot/astrology reading on-device: persistence, reading computation, and UI screens, driven by the existing engine/interpreter code linked in unchanged where possible. Required engine-side changes to make that linking work: - A compact low-precision ephemeris (lowprec_ephemeris.c) replacing the vendored ~127KB Astronomy Engine, which doesn't fit the watch's ~64KB app budget. - Hand-rolled sqrt/atan2/sin/cos replacements for that ephemeris and astro.c's Ascendant calculation - Pebble's statically-linked libm hard-faults on real hardware under this app's -fPIE link for all four. - narrative.c/guidance.c refactored from FILE*/fprintf onto snprintf- based buffers, since Pebble's SDK blocks fprintf at compile time.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
#include "guidance.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* i18n_get() only - see narrative.c's own comment on why this one extra
|
||||
* engine header is needed despite guidance.h's otherwise header-only
|
||||
* engine dependency. */
|
||||
@@ -137,13 +140,20 @@ static const char *reversed_marker(bool reversed) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
||||
void guidance_write(char *buf, size_t buf_size, const char *indent, const DailyInterpretation *interp,
|
||||
const CelticCrossSpread *spread) {
|
||||
if (buf_size == 0) return;
|
||||
buf[0] = '\0';
|
||||
|
||||
const TarotDraw *attitude = &spread->positions[POSITION_ATTITUDE];
|
||||
const TarotDraw *outcome = &spread->positions[POSITION_OUTCOME];
|
||||
|
||||
/* Chained snprintf() calls, each bounds-checked against the running
|
||||
* length before the next one runs - see narrative_write()'s matching
|
||||
* comment for why (never fprintf/sprintf/vsnprintf). */
|
||||
size_t len;
|
||||
if (interp->top_item_count == 0) {
|
||||
fprintf(out, "%s%s\n", indent, no_transit_text(attitude->reversed));
|
||||
len = (size_t)snprintf(buf, buf_size, "%s%s\n", indent, no_transit_text(attitude->reversed));
|
||||
} else {
|
||||
const Aspect *top = &interp->top_items[0].aspect;
|
||||
TransitCharacter character = classify_transit(top->type);
|
||||
@@ -152,13 +162,18 @@ void guidance_print(FILE *out, const char *indent, const DailyInterpretation *in
|
||||
char intro[192];
|
||||
snprintf(intro, sizeof(intro), intro_template(interp->day_level),
|
||||
planet_display_name(top->transiting_planet));
|
||||
fprintf(out, "%s%s %s\n", indent, intro, stance);
|
||||
len = (size_t)snprintf(buf, buf_size, "%s%s %s\n", indent, intro, stance);
|
||||
}
|
||||
if (len >= buf_size) return;
|
||||
|
||||
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.attitude_intro", "Your Attitude card is"),
|
||||
tarot_card_name(attitude->card), reversed_marker(attitude->reversed),
|
||||
tarot_card_meaning(attitude->card, attitude->reversed));
|
||||
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.outcome_intro", "The spread's likely Outcome is"),
|
||||
tarot_card_name(outcome->card), reversed_marker(outcome->reversed),
|
||||
tarot_card_meaning(outcome->card, outcome->reversed));
|
||||
len += (size_t)snprintf(buf + len, buf_size - len, "%s%s %s%s: %s\n", indent,
|
||||
i18n_get("guidance.attitude_intro", "Your Attitude card is"),
|
||||
tarot_card_name(attitude->card), reversed_marker(attitude->reversed),
|
||||
tarot_card_meaning(attitude->card, attitude->reversed));
|
||||
if (len >= buf_size) return;
|
||||
|
||||
snprintf(buf + len, buf_size - len, "%s%s %s%s: %s\n", indent,
|
||||
i18n_get("guidance.outcome_intro", "The spread's likely Outcome is"),
|
||||
tarot_card_name(outcome->card), reversed_marker(outcome->reversed),
|
||||
tarot_card_meaning(outcome->card, outcome->reversed));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef DECK_GUIDANCE_H
|
||||
#define DECK_GUIDANCE_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Header-only dependency on the engine for struct/enum *definitions*,
|
||||
* same policy as narrative.h/significance.h - see their comments for
|
||||
@@ -14,6 +14,13 @@
|
||||
#include "../../engine/src/tarot.h"
|
||||
#include "significance.h"
|
||||
|
||||
/* Generous upper bound on guidance_write()'s output (intro/stance line
|
||||
* + Attitude line + Outcome line, each ending in a newline, including
|
||||
* the longest tarot_card_meaning() strings) across every language this
|
||||
* project ships a translation for - see narrative.h's NARRATIVE_TEXT_MAX
|
||||
* for the same reasoning. */
|
||||
#define GUIDANCE_TEXT_MAX 1024
|
||||
|
||||
/* Ties the day's single most significant transit
|
||||
* (interp->top_items[0]) to the Celtic Cross spread, and prints a short
|
||||
* paragraph of concrete guidance on how to meet the day - the "combined
|
||||
@@ -49,12 +56,18 @@
|
||||
* strings) is looked up via i18n_get() under "guidance.*" keys
|
||||
* (engine/i18n's .lang files) before falling back to the English text baked
|
||||
* into guidance.c, so it respects whatever --lang the caller loaded
|
||||
* with i18n_load() (main.c does so before calling this). The German
|
||||
* guidance.intro.* templates are deliberately phrased so the planet
|
||||
* name placeholder is always the sentence's grammatical subject
|
||||
* (nominative case) across all four day levels - see guidance.c's own
|
||||
* comment on k_intro_fallback. */
|
||||
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
||||
* with i18n_load()/i18n_load_table() (main.c does so before calling
|
||||
* this). The German guidance.intro.* templates are deliberately phrased
|
||||
* so the planet name placeholder is always the sentence's grammatical
|
||||
* subject (nominative case) across all four day levels - see
|
||||
* guidance.c's own comment on k_intro_fallback.
|
||||
*
|
||||
* Writes into `buf` (a caller-supplied buffer of `buf_size` bytes,
|
||||
* always left null-terminated - GUIDANCE_TEXT_MAX is a safe size)
|
||||
* instead of a FILE*, using only snprintf() internally (never
|
||||
* fprintf/sprintf/vsnprintf, all of which Pebble's SDK blocks at compile
|
||||
* time), so this function links into the watch app unchanged. */
|
||||
void guidance_write(char *buf, size_t buf_size, const char *indent, const DailyInterpretation *interp,
|
||||
const CelticCrossSpread *spread);
|
||||
|
||||
#endif
|
||||
|
||||
+25
-20
@@ -1,5 +1,3 @@
|
||||
#define _POSIX_C_SOURCE 200809L /* for open_memstream */
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -156,27 +154,28 @@ static void print_body_in_sign_text(FILE *out, const PlanetPosition *pos) {
|
||||
fprintf(out, " %s", buf);
|
||||
}
|
||||
|
||||
/* Captures narrative_print()/guidance_print()'s output (they only know
|
||||
* how to write to a FILE*) into a heap string, for embedding into JSON -
|
||||
* avoids changing either module's public API just for this one caller.
|
||||
* Strips a single trailing newline; caller must free() the result. */
|
||||
/* Captures narrative_write()/guidance_write()'s output into a heap
|
||||
* string, for embedding into JSON - a stack buffer plus a single
|
||||
* malloc()+memcpy() copy, since main.c doesn't need to stream this
|
||||
* anywhere. Strips a single trailing newline; caller must free() the
|
||||
* result. */
|
||||
static char *capture_narrative(const Aspect *aspect, int house) {
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *mem = open_memstream(&buf, &size);
|
||||
narrative_print(mem, "", aspect, house);
|
||||
fclose(mem);
|
||||
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
||||
char stack_buf[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(stack_buf, sizeof stack_buf, "", aspect, house);
|
||||
size_t len = strlen(stack_buf);
|
||||
if (len > 0 && stack_buf[len - 1] == '\n') stack_buf[--len] = '\0';
|
||||
char *buf = malloc(len + 1);
|
||||
memcpy(buf, stack_buf, len + 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *mem = open_memstream(&buf, &size);
|
||||
guidance_print(mem, "", interp, spread);
|
||||
fclose(mem);
|
||||
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
||||
char stack_buf[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(stack_buf, sizeof stack_buf, "", interp, spread);
|
||||
size_t len = strlen(stack_buf);
|
||||
if (len > 0 && stack_buf[len - 1] == '\n') stack_buf[--len] = '\0';
|
||||
char *buf = malloc(len + 1);
|
||||
memcpy(buf, stack_buf, len + 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -228,7 +227,11 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
||||
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
|
||||
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
|
||||
ui("interp.score", "score"), item->score);
|
||||
narrative_print(out, " ", a, reading->transits.bodies[a->transiting_planet].house);
|
||||
|
||||
char narrative_buf[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(narrative_buf, sizeof narrative_buf, " ", a,
|
||||
reading->transits.bodies[a->transiting_planet].house);
|
||||
fputs(narrative_buf, out);
|
||||
}
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
@@ -236,7 +239,9 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
||||
print_celtic_cross_text(out, &reading->spread);
|
||||
fprintf(out, "\n");
|
||||
|
||||
guidance_print(out, "", interp, &reading->spread);
|
||||
char guidance_buf[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(guidance_buf, sizeof guidance_buf, "", interp, &reading->spread);
|
||||
fputs(guidance_buf, out);
|
||||
}
|
||||
|
||||
/* ===== --format html ===== */
|
||||
|
||||
@@ -136,8 +136,11 @@ static const char *narrative_field(const char *slug, const char *field, const ch
|
||||
return i18n_get(key, fallback);
|
||||
}
|
||||
|
||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||
void narrative_write(char *buf, size_t buf_size, const char *indent, const Aspect *aspect,
|
||||
int transiting_house) {
|
||||
if (buf_size == 0) return;
|
||||
buf[0] = '\0';
|
||||
|
||||
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
|
||||
const char *base = narrative_field(n->slug, "base", n->base);
|
||||
|
||||
@@ -147,7 +150,15 @@ void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||
|
||||
if (base[0] == '\0' && extra[0] == '\0') return;
|
||||
|
||||
fprintf(out, "%s", indent);
|
||||
/* Chained snprintf() calls, each bounds-checked against the running
|
||||
* length before the next one runs - never fprintf/sprintf/vsnprintf,
|
||||
* all of which Pebble's SDK blocks at compile time (see narrative.h's
|
||||
* doc comment). Once `len` reaches buf_size (truncated), every
|
||||
* subsequent call is skipped rather than computing buf_size - len,
|
||||
* which would underflow. */
|
||||
size_t len = (size_t)snprintf(buf, buf_size, "%s", indent);
|
||||
if (len >= buf_size) return;
|
||||
|
||||
if (transiting_house >= 1 && transiting_house <= 12) {
|
||||
char house_key[24];
|
||||
snprintf(house_key, sizeof house_key, "narrative.house.%d", transiting_house);
|
||||
@@ -156,9 +167,16 @@ void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||
char frame[256];
|
||||
snprintf(frame, sizeof frame, i18n_get("narrative.house_frame", "In matters of %s (house %d)."),
|
||||
area, transiting_house);
|
||||
fprintf(out, "%s ", frame);
|
||||
len += (size_t)snprintf(buf + len, buf_size - len, "%s ", frame);
|
||||
if (len >= buf_size) return;
|
||||
}
|
||||
if (base[0] != '\0') fprintf(out, "%s", base);
|
||||
if (extra[0] != '\0') fprintf(out, "%s%s", base[0] != '\0' ? " " : "", extra);
|
||||
fprintf(out, "\n");
|
||||
if (base[0] != '\0') {
|
||||
len += (size_t)snprintf(buf + len, buf_size - len, "%s", base);
|
||||
if (len >= buf_size) return;
|
||||
}
|
||||
if (extra[0] != '\0') {
|
||||
len += (size_t)snprintf(buf + len, buf_size - len, "%s%s", base[0] != '\0' ? " " : "", extra);
|
||||
if (len >= buf_size) return;
|
||||
}
|
||||
snprintf(buf + len, buf_size - len, "\n");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
#ifndef DECK_NARRATIVE_H
|
||||
#define DECK_NARRATIVE_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Header-only dependency on the engine, same policy as significance.h -
|
||||
* see its comment for why. */
|
||||
#include "../../engine/src/astro.h"
|
||||
|
||||
/* Generous upper bound on narrative_write()'s output (indent + house
|
||||
* framing + base + harmonious/discordant addendum + newline) across
|
||||
* every language this project ships a translation for - callers on
|
||||
* constrained platforms (the watch) are free to size their own stack
|
||||
* buffer smaller if they know their own strings are shorter; this is
|
||||
* just a safe default for a caller that doesn't want to think about it. */
|
||||
#define NARRATIVE_TEXT_MAX 512
|
||||
|
||||
/* 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_
|
||||
@@ -22,15 +30,19 @@
|
||||
* 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
|
||||
* Writes a narrative sentence for `aspect`'s transiting planet into
|
||||
* `buf` (a caller-supplied buffer of `buf_size` bytes, always left
|
||||
* null-terminated - NARRATIVE_TEXT_MAX is a safe size), indented with
|
||||
* `indent` and followed by a newline - or writes an empty string 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.
|
||||
* this data doesn't model. Uses only snprintf() internally (never
|
||||
* fprintf/sprintf/vsnprintf, all of which Pebble's SDK blocks at compile
|
||||
* time), so this function links into the watch app unchanged.
|
||||
*
|
||||
* `transiting_house` is the whole-sign house (1-12) the transiting
|
||||
* planet currently occupies in the natal chart - i.e.
|
||||
@@ -44,9 +56,9 @@
|
||||
* Every string here is looked up via i18n_get() under "narrative.*"
|
||||
* keys (engine/i18n's .lang files) before falling back to the English text
|
||||
* baked into narrative.c, the same catalog tarot_data.c uses - so this
|
||||
* text respects whatever --lang the caller loaded with i18n_load()
|
||||
* (main.c does so before calling this). */
|
||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||
* text respects whatever --lang the caller loaded with i18n_load()/
|
||||
* i18n_load_table() (main.c does so before calling this). */
|
||||
void narrative_write(char *buf, size_t buf_size, const char *indent, const Aspect *aspect,
|
||||
int transiting_house);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define _POSIX_C_SOURCE 200809L /* for mkstemp/fdopen */
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -9,15 +10,6 @@
|
||||
#include "../../engine/src/i18n.h"
|
||||
#include "../src/guidance.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 DailyInterpretation make_interp(DaySignificance level, AspectType type, Body transiting) {
|
||||
DailyInterpretation interp;
|
||||
interp.top_item_count = 1;
|
||||
@@ -51,9 +43,8 @@ static CelticCrossSpread make_spread(TarotCard attitude_card, bool attitude_reve
|
||||
static void test_harmonious_upright(void) {
|
||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_TRINE, PLANET_JUPITER);
|
||||
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "already meeting the day in the right spirit") != NULL);
|
||||
assert(strstr(text, "Jupiter") != NULL);
|
||||
@@ -61,39 +52,34 @@ static void test_harmonious_upright(void) {
|
||||
assert(strstr(text, "The World: Assured success") != NULL);
|
||||
assert(strstr(text, "(Reversed)") == NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_harmonious_upright\n");
|
||||
}
|
||||
|
||||
static void test_discordant_reversed(void) {
|
||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
||||
CelticCrossSpread spread = make_spread(CARD_DEVIL, true, CARD_WORLD, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "Both the day and your own footing are unsettled") != NULL);
|
||||
assert(strstr(text, "Saturn") != NULL);
|
||||
assert(strstr(text, "The Devil (Reversed): Evil fatality") != NULL);
|
||||
assert(strstr(text, "The World: Assured success") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_discordant_reversed\n");
|
||||
}
|
||||
|
||||
static void test_neutral_conjunction_upright(void) {
|
||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_CONJUNCTION, PLANET_PLUTO);
|
||||
CelticCrossSpread spread = make_spread(CARD_EMPEROR, false, CARD_MOON, true);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "Today concentrates whatever you already bring to it") != NULL);
|
||||
assert(strstr(text, "Pluto") != NULL);
|
||||
assert(strstr(text, "The Emperor: Stability, power") != NULL);
|
||||
assert(strstr(text, "The Moon (Reversed): Instability, inconstancy") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_neutral_conjunction_upright\n");
|
||||
}
|
||||
|
||||
@@ -104,74 +90,64 @@ static void test_neutral_conjunction_upright(void) {
|
||||
static void test_significant_day_uses_significant_intro(void) {
|
||||
DailyInterpretation interp = make_interp(DAY_SIGNIFICANT, ASPECT_SEXTILE, PLANET_VENUS);
|
||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "clearly active today") != NULL);
|
||||
assert(strstr(text, "Venus") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_significant_day_uses_significant_intro\n");
|
||||
}
|
||||
|
||||
static void test_notable_day_uses_notable_intro(void) {
|
||||
DailyInterpretation interp = make_interp(DAY_NOTABLE, ASPECT_SEXTILE, PLANET_MARS);
|
||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "mild touch from Mars") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_notable_day_uses_notable_intro\n");
|
||||
}
|
||||
|
||||
static void test_quiet_day_with_a_faint_item_still_names_it(void) {
|
||||
DailyInterpretation interp = make_interp(DAY_QUIET, ASPECT_SEXTILE, PLANET_MERCURY);
|
||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "only faintly active today") != NULL);
|
||||
assert(strstr(text, "Mercury") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_quiet_day_with_a_faint_item_still_names_it\n");
|
||||
}
|
||||
|
||||
static void test_no_aspects_at_all_falls_back_to_attitude_only(void) {
|
||||
DailyInterpretation interp = make_empty_interp();
|
||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "astrologically quiet day") != NULL);
|
||||
assert(strstr(text, "trust your current footing") != NULL);
|
||||
assert(strstr(text, "The Star: Loss, theft") != NULL);
|
||||
assert(strstr(text, "The Sun: Material happiness") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_no_aspects_at_all_falls_back_to_attitude_only\n");
|
||||
}
|
||||
|
||||
static void test_no_aspects_at_all_respects_reversed_attitude(void) {
|
||||
DailyInterpretation interp = make_empty_interp();
|
||||
CelticCrossSpread spread = make_spread(CARD_STAR, true, CARD_SUN, false);
|
||||
FILE *f = tmpfile();
|
||||
guidance_print(f, "", &interp, &spread);
|
||||
const char *text = slurp(f);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "quietly re-settle your own footing") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_no_aspects_at_all_respects_reversed_attitude\n");
|
||||
}
|
||||
|
||||
/* Proves guidance_print() actually routes through i18n_get() rather
|
||||
/* Proves guidance_write() actually routes through i18n_get() rather
|
||||
* than just printing the hardcoded fallback. Run last: i18n_load()
|
||||
* replaces the process-global catalog for the rest of the binary's
|
||||
* lifetime. */
|
||||
@@ -191,16 +167,14 @@ static void test_translation_catalog_overrides_fallback_text(void) {
|
||||
|
||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
||||
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
||||
FILE *out = tmpfile();
|
||||
guidance_print(out, "", &interp, &spread);
|
||||
const char *text = slurp(out);
|
||||
char text[GUIDANCE_TEXT_MAX];
|
||||
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||
|
||||
assert(strstr(text, "UEBERSETZTE HALTUNG") != NULL);
|
||||
assert(strstr(text, "UEBERSETZTE EINLEITUNG UEBERSETZTER SATURN") != NULL);
|
||||
assert(strstr(text, "UEBERSETZTE HALTUNGSKARTE") != NULL);
|
||||
assert(strstr(text, "Your instincts are sound") == NULL); /* English fallback must not leak through */
|
||||
|
||||
fclose(out);
|
||||
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -9,115 +9,92 @@
|
||||
#include "../../engine/src/i18n.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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 0);
|
||||
|
||||
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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 0);
|
||||
|
||||
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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 0);
|
||||
|
||||
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. */
|
||||
* conjunction to it should write 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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 0);
|
||||
|
||||
assert(len == 0);
|
||||
assert(text[0] == '\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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 0);
|
||||
|
||||
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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 3);
|
||||
|
||||
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);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, " ", &a, 0); /* 0 = "no data" sentinel, not a real house */
|
||||
|
||||
assert(strstr(text, "house") == NULL);
|
||||
assert(strstr(text, "depression") != NULL);
|
||||
|
||||
fclose(f);
|
||||
printf("PASS test_unknown_house_omits_area_framing\n");
|
||||
}
|
||||
|
||||
/* Proves narrative_print() actually routes through i18n_get() rather
|
||||
/* Proves narrative_write() actually routes through i18n_get() rather
|
||||
* than just printing the hardcoded fallback - loads a translation file
|
||||
* that overrides one planet's base text and one house's area text, and
|
||||
* checks the override wins. Run last: i18n_load() replaces the
|
||||
@@ -136,15 +113,13 @@ static void test_translation_catalog_overrides_fallback_text(void) {
|
||||
|
||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
||||
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
||||
FILE *out = tmpfile();
|
||||
narrative_print(out, "", &a, 3);
|
||||
const char *text = slurp(out);
|
||||
char text[NARRATIVE_TEXT_MAX];
|
||||
narrative_write(text, sizeof text, "", &a, 3);
|
||||
|
||||
assert(strstr(text, "UEBERSETZTER SATURN TEXT") != NULL);
|
||||
assert(strstr(text, "UEBERSETZTES HAUS DREI") != NULL);
|
||||
assert(strstr(text, "depression") == NULL); /* English fallback must not leak through */
|
||||
|
||||
fclose(out);
|
||||
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user