Files
deck_in_a_dash/CLAUDE.md
T
ml e4ea31270f 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.
2026-07-04 10:59:30 +02:00

9.2 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project goal

A Pebble smartwatch app that produces a daily personalized tarot (Celtic Cross) and astrology (natal chart + transits) reading. Currently only the core engine exists: a plain-C, dependency-free library plus a standalone CLI binary (dist/deck-engine) that can be built and run without any Pebble SDK or emulator. The watchapp itself (Pebble C/UI, resource packs) has not been started yet — the engine is deliberately built so it can be linked straight into it later without rework (see "Portability to the watch" below).

Build & run

cd engine
make          # builds dist/deck-engine, dist/img/, dist/i18n/, dist/run.sh, dist/user.properties
make test     # builds and runs engine/tests/smoke_test.c
make clean    # removes build/ and the generated binary/img/i18n (never touches dist/user.properties)

Build output goes to dist/, a sibling of engine/ and res/ (not inside engine/). There is no make install; dist/ is meant to be run in place.

Two ways to run a reading:

# 1. Direct CLI, all inputs explicit:
dist/deck-engine --seed "2026-07-03" \
  --birth-date 1990-05-14 --birth-time 14:32 --birth-utc-offset 2 \
  --birth-lat 52.5200 --birth-lon 13.4050 \
  [--date YYYY-MM-DDTHH:MM] [--format text|html] [--lang en|de] [--i18n-dir <path>]

# 2. dist/run.sh [text|html] [lang] — wraps the binary for daily use: seed
#    and --date come from the OS clock (today's date / current UTC time),
#    birth data + default language are read from dist/user.properties
#    next to the script; the optional [lang] arg overrides that language
#    for a single run.

dist/user.properties is seeded once from engine/scripts/user.properties.template with placeholder values (YOUR_BIRTH_DATE_HERE, etc.) and is never overwritten by later buildsrun.sh refuses to run (with a clear error) until the placeholders are replaced with real values.

Run a single smoke test by editing engine/tests/smoke_test.c's main() temporarily, or just read its assertions — there's no test filter flag, the whole suite runs in milliseconds.

Architecture

deck_in_a_dash/
  res/                          Tarot card art (22 RWS Major Arcana JPEGs),
                                 Waite's "Pictorial Key to the Tarot" (PDF,
                                 public domain — source of all card/spread
                                 text), logo, research notes.
  engine/
    third_party/astronomy/      Vendored cosinekitty/astronomy C library
                                 (MIT, pinned commit — see VENDORED.md).
                                 Unmodified from upstream.
    src/
      rng.h/.c                  Deterministic string-seeded PRNG.
      tarot.h/.c                Celtic Cross spread draw logic.
      tarot_data.c               22 cards' names/meanings + 10 position
                                 names/descriptions (Waite's own text).
      astro.h/.c                 Natal chart + daily transits.
      i18n.h/.c                   key=value translation catalog + English fallback.
      reading.h/.c               Ties tarot + astro together; text/HTML output.
      main.c                     CLI entry point.
    i18n/en.lang, i18n/de.lang   Translation source files, one per language.
    tests/smoke_test.c
    scripts/run.sh, scripts/user.properties.template
    Makefile
  dist/                         Build output (gitignored-style; see Build & run).

Data flow / the real API

reading_generate(seed, birth, utc_moment, &DailyReading) in reading.h is the single entry point that matters — it's what the watchapp will call once it exists. It populates a plain struct (NatalChart + DailyTransits + CelticCrossSpread) that the caller walks directly to build a UI. reading_print_text/reading_print_html are desktop-only conveniences for inspecting a reading (used by the CLI); the watchapp will never call them.

Determinism (rng.c)

The Celtic Cross draw must be exactly reproducible from a seed string, on any platform (including the watch's ARM core later), so rng.c is pure integer arithmetic: FNV-1a hashes the seed string, splitmix64 streams from it, and a partial FisherYates shuffle (tarot.c) draws the 10 cards plus a reversal bit per card from that same stream, in one pass.

Known-sharp-edge: rng_next_bounded's rejection-sampling limit must stay a uint64_t. When bound evenly divides 2^32 (true for bound == 2, used by every reversed-card coin flip), the true rejection threshold is 2^32, which silently truncates to 0 in a uint32_t and turns the loop into an infinite one. This exact bug shipped once already — if you touch this function, keep the intermediate type 64-bit.

Astrology (astro.c)

  • Bodies: Sun, Moon, MercuryPluto (Body enum in astro.h, prefixed PLANET_* — deliberately not BODY_*, because Astronomy Engine's own astro_body_t already defines BODY_SUN, BODY_MOON, etc., and both headers are included together in astro.c).
  • Positions must go through Astronomy_GeoVector + Astronomy_Ecliptic, not Astronomy_EclipticLongitude. The latter computes heliocentric longitude and outright rejects BODY_SUN (ASTRO_INVALID_BODY) — wrong for astrology, which needs apparent geocentric position. This also shipped once and was caught by the natal-Sun-sign smoke test.
  • Houses use the Whole Sign system (house 1 = the Ascendant's sign, houses follow zodiacally) — no Placidus/Koch cusp trig. Astronomy Engine has no Ascendant function; it's computed directly in astro.c from sidereal time + a standard low-precision obliquity polynomial + the RAMC/obliquity/latitude identity (Duffett-Smith & Zwart).
  • Aspects (conjunction/sextile/square/trine/opposition) use an 8° orb when a luminary (Sun/Moon) is involved, 6° otherwise.

Tarot (tarot.c, tarot_data.c)

Only the 22 Major Arcana are modelled (TAROT_DECK_SIZE) — matches the art actually in res/img/. The 10 Celtic Cross positions (CelticCrossPosition in tarot.h) follow Waite's own original drawing order from The Pictorial Key to the Tarot, Part III §7 ("An Ancient Celtic Method of Divination"): Present → Challenge → Crown → Foundation → Recent Past → Near Future → Attitude → Environment → Hopes/Fears → Outcome. Note this differs from the reordering used by most modern tutorials (which typically put Foundation/Recent Past before Crown) — res/2026_07_03_celtic_cross_spread.txt has an example of that more common (but less original) ordering; don't use it as a reference for the position enum order. Card and position text in tarot_data.c is condensed from Waite's own wording in Part III §3 (public domain).

Translations (i18n.c, engine/i18n/*.lang)

All display text (planet/sign/moon-phase/aspect names, tarot card names and meanings, Celtic Cross position names, and reading.c's section headers) is looked up in a small process-global catalog loaded by i18n_load(path) from a key=value file, via i18n_get(key, fallback). Every call site passes the hardcoded English string as fallback, so a missing file, a missing key, or an untranslated language degrades to English rather than showing a raw key or nothing — this is what lets engine/i18n/de.lang (or any future language) be filled in incrementally.

Keys are namespaced by the slug tables in tarot_data.c/astro.c (card.<slug>.name, card.<slug>.upright/reversed, position.<slug>.name/desc, body.<slug>, sign.<slug>, moonphase.<slug>, aspect.<slug>), plus ui.* keys for reading.c's own labels — deliberately keyed off separate slug strings rather than the C enum names, so renaming an enumerator can never silently break a .lang file. To add a language: copy engine/i18n/en.lang to <code>.lang in the same directory (keys must match exactly) and translate the values — no source changes needed, make picks up any *.lang file there automatically.

i18n_load uses stdio (fopen/fgets), so — like main.c and reading_print_* — it's a desktop-only entry point; i18n_get itself is a pure fixed-size-array lookup with no heap allocation, so it's fine to port to the watch once it has its own (non-file-based) way to populate the catalog, e.g. from a compiled-in resource.

Portability to the watch

rng, tarot, tarot_data, astro, i18n_get, and reading_generate are kept free of stdio/CLI assumptions specifically so they can link into the Pebble watchapp unchanged later. Only reading_print_text/ _html (text formatting), main.c (CLI arg parsing), and i18n_load (reads a file from disk) are desktop-only and won't port as-is.

Licensing

  • Engine code (engine/src/, engine/scripts/, engine/Makefile): MIT, per the repo's top-level LICENSE.
  • engine/third_party/astronomy/: vendored MIT code, unmodified — see VENDORED.md for the pinned upstream commit.
  • Card/spread text in tarot_data.c/engine/i18n/en.lang: condensed from A. E. Waite's The Pictorial Key to the Tarot (1911), public domain.
  • engine/i18n/de.lang's card/spread text is an original translation of that same public-domain English text, made for this project — not taken from any specific published German edition.