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

189 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
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:
```bash
# 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 builds** — `run.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.