Introduces watch/, a real Pebble watchapp (built via pebble build) that
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:
ml
2026-07-14 17:10:22 +02:00
parent d9b6e2889b
commit 1a654da3e9
44 changed files with 1920 additions and 182 deletions
+19 -44
View File
@@ -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");
}