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:
@@ -0,0 +1,188 @@
|
||||
# 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 Fisher–Yates 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, Mercury–Pluto (`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.
|
||||
Reference in New Issue
Block a user