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.
@@ -0,0 +1,10 @@
|
|||||||
|
# Build output, including dist/user.properties which holds real birth
|
||||||
|
# data once filled in — never commit it.
|
||||||
|
/dist
|
||||||
|
/engine/build
|
||||||
|
|
||||||
|
*.o
|
||||||
|
core
|
||||||
|
core.*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Matthias Ladkau
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="res/logo.png" width="200" alt="Deck in a Dash logo">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Deck in a Dash
|
||||||
|
|
||||||
|
A Pebble smartwatch app that produces a daily personalized tarot (Celtic
|
||||||
|
Cross) and astrology (natal chart + transits) reading.
|
||||||
|
|
||||||
|
**Status:** only the core engine exists so far — a plain-C, dependency-free
|
||||||
|
library and standalone CLI (`dist/deck-engine`) that computes a full
|
||||||
|
reading without needing a Pebble device, emulator, or SDK. The watchapp
|
||||||
|
itself hasn't been built yet; the engine is designed so it can be linked
|
||||||
|
into it later without rework.
|
||||||
|
|
||||||
|
## What it computes
|
||||||
|
|
||||||
|
- **Astrology**: a personalized natal chart (Sun through Pluto, plus the
|
||||||
|
Ascendant and whole-sign houses) from a birth date, time, and location,
|
||||||
|
and the day's transits — including moon phase and aspects — against
|
||||||
|
that chart.
|
||||||
|
- **Tarot**: a 10-card Celtic Cross spread, drawn from the 22 Major
|
||||||
|
Arcana, with upright/reversed orientation per card. The full spread is
|
||||||
|
deterministic: the same seed string always produces the same cards, in
|
||||||
|
the same positions, with the same orientations. Card meanings and
|
||||||
|
spread positions are sourced from A. E. Waite's *The Pictorial Key to
|
||||||
|
the Tarot* (1911, public domain), in his own original drawing order.
|
||||||
|
|
||||||
|
Readings are available in **English and German** (`--lang en`/`--lang
|
||||||
|
de`, or `lang=` in `user.properties`); adding another language is just
|
||||||
|
dropping a translated `key=value` file into `engine/i18n/` — see
|
||||||
|
[`docs/input-output-format.md`](docs/input-output-format.md).
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
res/ Tarot card art (22 RWS Major Arcana), the
|
||||||
|
Waite PDF, logo, research notes.
|
||||||
|
engine/
|
||||||
|
third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT).
|
||||||
|
src/ Engine source (rng, tarot, astro, i18n, reading, CLI).
|
||||||
|
i18n/ Translation files (en.lang, de.lang, ...).
|
||||||
|
tests/smoke_test.c Determinism, ephemeris, and i18n regression checks.
|
||||||
|
scripts/ run.sh and the user.properties template.
|
||||||
|
Makefile
|
||||||
|
dist/ Build output (gitignored) — see below.
|
||||||
|
docs/ Architecture/dataflow diagrams, input/output format reference.
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`CLAUDE.md`](CLAUDE.md) for the detailed architecture and the sharp
|
||||||
|
edges worth knowing before changing the engine, or the
|
||||||
|
[`docs/`](docs/) directory for diagrams
|
||||||
|
([`docs/architecture.md`](docs/architecture.md)) and the full CLI/file/
|
||||||
|
struct format reference
|
||||||
|
([`docs/input-output-format.md`](docs/input-output-format.md)).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd engine
|
||||||
|
make # -> dist/deck-engine, dist/img/, dist/i18n/, dist/run.sh, dist/user.properties
|
||||||
|
make test # builds and runs engine/tests/smoke_test.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires only a C99 compiler and `make` — no other dependencies.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
**Daily use**, via `dist/run.sh`: pulls today's date and the current UTC
|
||||||
|
time from the OS clock (used as the tarot seed and the transit moment)
|
||||||
|
and reads your birth data from `dist/user.properties`. Edit that file
|
||||||
|
first — it's seeded once from a placeholder template and is never
|
||||||
|
overwritten by later builds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dist/run.sh # text output, language from user.properties (default en)
|
||||||
|
dist/run.sh html # HTML report with card art, open in a browser
|
||||||
|
dist/run.sh html de # override the language for this run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Direct CLI**, with every input explicit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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]
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The engine source code in this repository (`engine/src/`, `engine/scripts/`,
|
||||||
|
`engine/Makefile`) is released under the **MIT License** — see
|
||||||
|
[`LICENSE`](LICENSE).
|
||||||
|
|
||||||
|
`engine/third_party/astronomy/` is vendored from
|
||||||
|
[cosinekitty/astronomy](https://github.com/cosinekitty/astronomy) (MIT,
|
||||||
|
unmodified) — see its `VENDORED.md` for the pinned commit.
|
||||||
|
|
||||||
|
Card and spread text in `engine/src/tarot_data.c` is condensed from A. E.
|
||||||
|
Waite's *The Pictorial Key to the Tarot* (1911), which is in the public
|
||||||
|
domain.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
See [`CLAUDE.md`](../CLAUDE.md) for build commands and the sharp edges to
|
||||||
|
know before changing the engine. This document is the visual overview.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **`res/`** holds the source assets: the 22 Rider-Waite-Smith Major
|
||||||
|
Arcana card images and A. E. Waite's *Pictorial Key to the Tarot* PDF
|
||||||
|
(public domain), which `tarot_data.c`'s card and position text is
|
||||||
|
condensed from.
|
||||||
|
- **`engine/third_party/astronomy/`** is a vendored, unmodified copy of
|
||||||
|
[cosinekitty/astronomy](https://github.com/cosinekitty/astronomy)
|
||||||
|
(MIT), pinned to a specific commit — see its `VENDORED.md`.
|
||||||
|
- **`engine/src/`** is the core engine. Everything except `main.c`,
|
||||||
|
`reading.c`'s `reading_print_text`/`reading_print_html` functions, and
|
||||||
|
`i18n.c`'s file-loading half is free of `stdio`/CLI assumptions,
|
||||||
|
specifically so it can be linked into the Pebble watchapp later without
|
||||||
|
rework.
|
||||||
|
- `rng.c` — deterministic string-seeded PRNG (FNV-1a + splitmix64).
|
||||||
|
- `tarot.c` / `tarot_data.c` — the Celtic Cross draw and its content.
|
||||||
|
- `astro.c` — natal chart + daily transits, built on the vendored
|
||||||
|
Astronomy Engine.
|
||||||
|
- `i18n.c` — loads a `key=value` translation file into a small
|
||||||
|
fixed-size catalog; every display-text lookup elsewhere falls back to
|
||||||
|
the built-in English string if no catalog is loaded or a key is
|
||||||
|
missing from it.
|
||||||
|
- `reading.c` — orchestrates the above into one `DailyReading` and
|
||||||
|
(desktop-only) renders it as text or HTML.
|
||||||
|
- `main.c` — CLI argument parsing, the only consumer of `reading_print_*`.
|
||||||
|
- **`engine/i18n/`** holds the translation source files (`en.lang`,
|
||||||
|
`de.lang`, ...), one `key=value` file per language — see
|
||||||
|
[`CLAUDE.md`](../CLAUDE.md) for the key-naming convention.
|
||||||
|
- **`dist/`** is generated by `make`: the `deck-engine` binary, a copy of
|
||||||
|
the card art under `img/`, a copy of the translation files under
|
||||||
|
`i18n/`, `run.sh`, and a `user.properties` template that's seeded once
|
||||||
|
and never overwritten by later builds.
|
||||||
|
- **The Pebble watchapp does not exist yet.** When it's built, it will
|
||||||
|
call `reading_generate()` directly and walk the returned struct to lay
|
||||||
|
out its own screens — it has no reason to touch `reading_print_*`.
|
||||||
|
|
||||||
|
## Dataflow
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
A single reading is produced by one call: `reading_generate(seed, birth,
|
||||||
|
utc_moment, &out)` in `reading.c`. It fans out to three independent
|
||||||
|
computations and combines their results:
|
||||||
|
|
||||||
|
1. **`astro_compute_natal_chart(birth)`** → `NatalChart` — geocentric
|
||||||
|
ecliptic sign/degree for the Sun through Pluto, the Ascendant, and
|
||||||
|
whole-sign houses, all from the birth date/time/location.
|
||||||
|
2. **`astro_compute_daily_transits(utc_moment, natal_chart)`** →
|
||||||
|
`DailyTransits` — today's planetary positions, the Moon phase, and
|
||||||
|
every transiting-to-natal aspect within orb. Depends on the natal
|
||||||
|
chart (read-only) to compute aspects against it.
|
||||||
|
3. **`tarot_draw_celtic_cross(seed)`** → `CelticCrossSpread` — ten cards
|
||||||
|
with position and upright/reversed orientation, fully determined by
|
||||||
|
the seed string alone.
|
||||||
|
|
||||||
|
These three results are combined into one `DailyReading` struct, which is
|
||||||
|
either rendered by `reading_print_text`/`reading_print_html` (used by the
|
||||||
|
CLI) or, in the future, walked directly by watchapp UI code. See
|
||||||
|
[`input-output-format.md`](input-output-format.md) for the exact fields
|
||||||
|
and output formats.
|
||||||
|
|
||||||
|
Every display string produced along the way (`tarot_data.c`'s card and
|
||||||
|
position text, `astro.c`'s body/sign/moon-phase/aspect names, and
|
||||||
|
`reading.c`'s section headers) is looked up in `i18n.c`'s translation
|
||||||
|
catalog, with the built-in English text as the fallback. `main.c` loads
|
||||||
|
the catalog from `--lang`/`--i18n-dir` (or `run.sh`'s `lang=` property)
|
||||||
|
before calling `reading_generate`, so this is orthogonal to the three
|
||||||
|
computations above — it only affects how their results are rendered as
|
||||||
|
text, not the underlying `TarotCard`/`ZodiacSign`/etc. enum values.
|
||||||
|
|
||||||
|
Both entry points (direct CLI flags, or `run.sh` pulling the seed/date
|
||||||
|
from the OS clock and birth data from `user.properties`) converge on the
|
||||||
|
same `main.c` argument parsing before calling `reading_generate` — there
|
||||||
|
is exactly one code path from parsed input to a reading.
|
||||||
|
|
||||||
|
## Regenerating the diagrams
|
||||||
|
|
||||||
|
The diagrams are Graphviz sources in `docs/diagrams/`, rendered to PNG:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs
|
||||||
|
dot -Tpng -Gdpi=150 diagrams/architecture.dot -o images/architecture.png
|
||||||
|
dot -Tpng -Gdpi=150 diagrams/dataflow.dot -o images/dataflow.png
|
||||||
|
```
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
digraph architecture {
|
||||||
|
rankdir=LR;
|
||||||
|
pad="0.3";
|
||||||
|
fontname="Helvetica";
|
||||||
|
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=12, margin="0.15,0.1"];
|
||||||
|
edge [fontname="Helvetica", fontsize=10];
|
||||||
|
|
||||||
|
labelloc="t";
|
||||||
|
label="Deck in a Dash — component architecture";
|
||||||
|
fontsize=16;
|
||||||
|
|
||||||
|
subgraph cluster_res {
|
||||||
|
label="res/ (source assets)";
|
||||||
|
style="dashed"; color=gray40; fontsize=11;
|
||||||
|
images [label="22 RWS Major Arcana\nJPEGs (res/img/)", fillcolor="#fdf3d0"];
|
||||||
|
pdf [label="Waite's \"Pictorial Key\nto the Tarot\" (PDF,\npublic domain)", fillcolor="#fdf3d0"];
|
||||||
|
{ rank=same; images; pdf; }
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_thirdparty {
|
||||||
|
label="engine/third_party/astronomy/";
|
||||||
|
style="dashed"; color=gray40; fontsize=11;
|
||||||
|
astronomy [label="Astronomy Engine\n(vendored, MIT,\npinned commit)", fillcolor="#fdf3d0"];
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_i18nsrc {
|
||||||
|
label="engine/i18n/ (translation sources)";
|
||||||
|
style="dashed"; color=gray40; fontsize=11;
|
||||||
|
langfiles [label="en.lang, de.lang, ...\nkey=value per language", fillcolor="#fdf3d0"];
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_engine {
|
||||||
|
label="engine/src/ (core engine — plain C, no I/O assumptions except reading_print_*/main.c/i18n_load)";
|
||||||
|
style="solid"; color=black; fontsize=11;
|
||||||
|
|
||||||
|
rng [label="rng.c\nstring-seeded PRNG\n(FNV-1a + splitmix64)", fillcolor="#cfe8fb"];
|
||||||
|
tarot [label="tarot.c\nCeltic Cross draw", fillcolor="#cfe8fb"];
|
||||||
|
tarot_data [label="tarot_data.c\ncard + position text", fillcolor="#cfe8fb"];
|
||||||
|
astro [label="astro.c\nnatal chart + transits", fillcolor="#cfe8fb"];
|
||||||
|
i18n [label="i18n.c\ntranslation catalog\n+ English fallback", fillcolor="#cfe8fb"];
|
||||||
|
reading [label="reading.c\nDailyReading\norchestration + text/HTML", fillcolor="#c8f0c8"];
|
||||||
|
main [label="main.c\nCLI arg parsing", fillcolor="#e3e3e3"];
|
||||||
|
{ rank=same; main; reading; }
|
||||||
|
{ rank=same; tarot; astro; i18n; }
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_dist {
|
||||||
|
label="dist/ (build output, generated by `make`)";
|
||||||
|
style="dashed"; color=gray40; fontsize=11;
|
||||||
|
binary [label="deck-engine", fillcolor="#fde0d0"];
|
||||||
|
distimg [label="img/\n(copied JPEGs)", fillcolor="#fde0d0"];
|
||||||
|
disti18n [label="i18n/\n(copied .lang files)", fillcolor="#fde0d0"];
|
||||||
|
runsh [label="run.sh", fillcolor="#fde0d0"];
|
||||||
|
props [label="user.properties\n(birth data + lang)", fillcolor="#fde0d0"];
|
||||||
|
{ rank=same; binary; distimg; disti18n; runsh; props; }
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_watch {
|
||||||
|
label="Pebble watchapp — not built yet";
|
||||||
|
style="dashed"; color=firebrick; fontsize=11; fontcolor=firebrick;
|
||||||
|
watch [label="watchapp C / UI\n(future)", fillcolor=white, color=firebrick, fontcolor=firebrick, style="rounded,dashed"];
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf -> tarot_data [label="sourced from", style=dotted, color=gray40];
|
||||||
|
|
||||||
|
tarot -> rng;
|
||||||
|
tarot -> tarot_data;
|
||||||
|
astro -> astronomy;
|
||||||
|
tarot_data -> i18n [label="looks up\ntranslated text", style=dotted, color=gray40];
|
||||||
|
astro -> i18n [label="looks up\ntranslated text", style=dotted, color=gray40];
|
||||||
|
reading -> i18n [label="looks up\nUI labels", style=dotted, color=gray40];
|
||||||
|
reading -> tarot;
|
||||||
|
reading -> astro;
|
||||||
|
main -> reading [label="CLI only"];
|
||||||
|
main -> i18n [label="i18n_load(\n--lang, --i18n-dir)"];
|
||||||
|
|
||||||
|
main -> binary [label="`make` builds", style=dotted, color=gray40];
|
||||||
|
images -> distimg [label="`make images`\ncopies", style=dotted, color=gray40];
|
||||||
|
langfiles -> disti18n [label="`make i18n`\ncopies", style=dotted, color=gray40];
|
||||||
|
runsh -> props [label="reads at\nrun time", style=dotted, color=gray40];
|
||||||
|
|
||||||
|
watch -> reading [label="will call\nreading_generate()\ndirectly", style=dashed, color=firebrick, fontcolor=firebrick];
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
digraph dataflow {
|
||||||
|
rankdir=TB;
|
||||||
|
pad="0.3";
|
||||||
|
fontname="Helvetica";
|
||||||
|
node [fontname="Helvetica", fontsize=12, margin="0.15,0.1"];
|
||||||
|
edge [fontname="Helvetica", fontsize=10];
|
||||||
|
|
||||||
|
labelloc="t";
|
||||||
|
label="Deck in a Dash — dataflow for one reading";
|
||||||
|
fontsize=16;
|
||||||
|
|
||||||
|
node [shape=box, style="rounded,filled"];
|
||||||
|
|
||||||
|
subgraph cluster_inputs {
|
||||||
|
label="Inputs (two equivalent entry points)"; style=dashed; color=gray40; fontsize=11;
|
||||||
|
cli [label="Direct CLI flags\n--seed --birth-* --date --format", fillcolor="#e3e3e3"];
|
||||||
|
runsh [label="run.sh:\nOS clock -> seed + --date\nuser.properties -> birth-*", fillcolor="#e3e3e3"];
|
||||||
|
}
|
||||||
|
|
||||||
|
main [label="main.c\nparses args into:\nseed (string), BirthData, utc_moment", fillcolor="#cfe8fb"];
|
||||||
|
|
||||||
|
reading_generate [label="reading_generate()\nreading.c", shape=box, style="rounded,filled", fillcolor="#9fd3f5", peripheries=2];
|
||||||
|
|
||||||
|
natal_fn [label="astro_compute_natal_chart()\nastro.c", fillcolor="#cfe8fb"];
|
||||||
|
transit_fn [label="astro_compute_daily_transits()\nastro.c", fillcolor="#cfe8fb"];
|
||||||
|
tarot_fn [label="tarot_draw_celtic_cross()\ntarot.c", fillcolor="#cfe8fb"];
|
||||||
|
|
||||||
|
natal_data [label="NatalChart\nbodies[10]: sign + degree\nascendant_longitude\nhouses[12] (whole-sign)", shape=note, fillcolor="#fdf3d0"];
|
||||||
|
transit_data [label="DailyTransits\nbodies[10]\nmoon_phase\naspects[]: transiting/natal\nplanet, type, orb", shape=note, fillcolor="#fdf3d0"];
|
||||||
|
spread_data [label="CelticCrossSpread\npositions[10]: card, reversed", shape=note, fillcolor="#fdf3d0"];
|
||||||
|
|
||||||
|
daily_reading [label="DailyReading\n{ natal, transits, spread }", shape=note, fillcolor="#fbe3c8", peripheries=2];
|
||||||
|
|
||||||
|
print_text [label="reading_print_text()", fillcolor="#cfe8fb"];
|
||||||
|
print_html [label="reading_print_html()", fillcolor="#cfe8fb"];
|
||||||
|
watch_fn [label="watchapp UI code\n(future — walks the struct\ndirectly, no print_* call)", fillcolor=white, color=firebrick, fontcolor=firebrick, style="rounded,dashed"];
|
||||||
|
|
||||||
|
out_text [label="stdout: plain-text report", shape=parallelogram, fillcolor="#c8f0c8"];
|
||||||
|
out_html [label="stdout: reading.html\n(<img src=\"img/...\"> per card)", shape=parallelogram, fillcolor="#c8f0c8"];
|
||||||
|
out_watch [label="watch screens\n(future)", shape=parallelogram, fillcolor=white, color=firebrick, fontcolor=firebrick, style="dashed"];
|
||||||
|
|
||||||
|
cli -> main;
|
||||||
|
runsh -> main;
|
||||||
|
main -> reading_generate [label="seed, birth,\nutc_moment"];
|
||||||
|
|
||||||
|
reading_generate -> natal_fn [label="birth"];
|
||||||
|
natal_fn -> natal_data;
|
||||||
|
|
||||||
|
reading_generate -> transit_fn [label="utc_moment,\nNatalChart"];
|
||||||
|
natal_data -> transit_fn [style=dotted, color=gray40, label="natal chart\nread-only"];
|
||||||
|
transit_fn -> transit_data;
|
||||||
|
|
||||||
|
reading_generate -> tarot_fn [label="seed"];
|
||||||
|
tarot_fn -> spread_data;
|
||||||
|
|
||||||
|
natal_data -> daily_reading;
|
||||||
|
transit_data -> daily_reading;
|
||||||
|
spread_data -> daily_reading;
|
||||||
|
|
||||||
|
daily_reading -> print_text;
|
||||||
|
daily_reading -> print_html;
|
||||||
|
daily_reading -> watch_fn [style=dashed, color=firebrick];
|
||||||
|
|
||||||
|
print_text -> out_text;
|
||||||
|
print_html -> out_html;
|
||||||
|
watch_fn -> out_watch [style=dashed, color=firebrick];
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 270 KiB |
@@ -0,0 +1,155 @@
|
|||||||
|
# Input & output formats
|
||||||
|
|
||||||
|
See [`architecture.md`](architecture.md) for how these fit together.
|
||||||
|
There are two ways to provide input (direct CLI flags, or `run.sh` +
|
||||||
|
`user.properties`) and two output formats (`text`, `html`); the
|
||||||
|
`DailyReading` struct is the programmatic form both outputs are rendered
|
||||||
|
from, and the one the future watchapp will consume directly.
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
### Direct CLI flags (`dist/deck-engine`)
|
||||||
|
|
||||||
|
```
|
||||||
|
deck-engine --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM
|
||||||
|
--birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>
|
||||||
|
[--date YYYY-MM-DDTHH:MM] [--format text|html]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Required | Format | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--seed` | yes | any string | Tarot seed. The same seed always produces the same 10-card spread, positions, and orientations (see `rng.c`/`tarot.c`). |
|
||||||
|
| `--birth-date` | yes | `YYYY-MM-DD` | Birth date, local calendar. |
|
||||||
|
| `--birth-time` | yes | `HH:MM` (24h) | Birth time, local clock. |
|
||||||
|
| `--birth-utc-offset` | yes | decimal hours, e.g. `2` or `-5.5` | Hours to **subtract** from local birth time to get UTC (e.g. `2` for CEST). |
|
||||||
|
| `--birth-lat` | yes | decimal degrees | Birth latitude, north positive. |
|
||||||
|
| `--birth-lon` | yes | decimal degrees | Birth longitude, east positive. |
|
||||||
|
| `--date` | no | `YYYY-MM-DDTHH:MM[:SS]` | UTC moment used for today's transits and as the moment the tarot seed is drawn against. Defaults to the current system time. |
|
||||||
|
| `--format` | no | `text` \| `html` | Output format, defaults to `text`. |
|
||||||
|
| `--lang` | no | language code, e.g. `en`, `de` | Reading language, defaults to `en`. Matches a file named `<code>.lang` in `--i18n-dir` (see "Translations" below). Unknown codes or missing files fall back to English with a warning on stderr. |
|
||||||
|
| `--i18n-dir` | no | path | Directory to look for `<lang>.lang` in. Defaults to an `i18n/` directory next to the binary itself (resolved from `argv[0]`, so `dist/deck-engine` finds `dist/i18n/` regardless of the caller's working directory). |
|
||||||
|
|
||||||
|
Birth latitude/longitude only affect the Ascendant/houses — planet signs
|
||||||
|
are geocentric and location-independent.
|
||||||
|
|
||||||
|
### `run.sh` + `user.properties`
|
||||||
|
|
||||||
|
`dist/run.sh [text|html] [lang]` wraps the binary for daily use:
|
||||||
|
|
||||||
|
- **Seed and `--date`** come from the OS clock: `date +%Y-%m-%d` (stable
|
||||||
|
all day) and `date -u +%Y-%m-%dT%H:%M`.
|
||||||
|
- **Birth data and language** are read from `dist/user.properties`, a
|
||||||
|
`key=value` properties file next to the script (`#`-prefixed lines and
|
||||||
|
blank lines are comments; whitespace around `=` is trimmed):
|
||||||
|
|
||||||
|
```properties
|
||||||
|
birth_date=1990-05-14
|
||||||
|
birth_time=14:32
|
||||||
|
birth_utc_offset=2
|
||||||
|
birth_lat=52.5200
|
||||||
|
birth_lon=13.4050
|
||||||
|
lang=en
|
||||||
|
```
|
||||||
|
|
||||||
|
The file 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` checks every birth field for an
|
||||||
|
empty value or a leftover `YOUR_` placeholder and refuses to run with a
|
||||||
|
clear error until the file is filled in; `lang` has no such check since
|
||||||
|
it always has a usable default (`en`).
|
||||||
|
- The optional second argument overrides `lang=` for a single run without
|
||||||
|
editing the file, e.g. `dist/run.sh html de`.
|
||||||
|
|
||||||
|
### Translations (`--lang`, `dist/i18n/`)
|
||||||
|
|
||||||
|
Every piece of display text — planet/sign/moon-phase/aspect names, tarot
|
||||||
|
card names and meanings, Celtic Cross position names, and the section
|
||||||
|
headers in the `text`/`html` output — is looked up in a translation
|
||||||
|
catalog with the built-in English text as the fallback for any key the
|
||||||
|
catalog doesn't have. See `CLAUDE.md`'s "Translations" section for the
|
||||||
|
key-naming convention and how to add a new language.
|
||||||
|
|
||||||
|
`engine/i18n/en.lang` and `de.lang` are the shipped translation files;
|
||||||
|
`make` copies `engine/i18n/*.lang` to `dist/i18n/` (refreshed on every
|
||||||
|
build, like `dist/img/`). Adding a language is just dropping another
|
||||||
|
`<code>.lang` file into `engine/i18n/` — no code changes needed.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
### `DailyReading` (the real output — `reading.h`)
|
||||||
|
|
||||||
|
`reading_generate()` is the actual API; `text`/`html` below are just two
|
||||||
|
ways of rendering the struct it fills in. This is what the watchapp will
|
||||||
|
read directly once it exists.
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
NatalChart natal;
|
||||||
|
DailyTransits transits;
|
||||||
|
CelticCrossSpread spread;
|
||||||
|
} DailyReading;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`NatalChart`** (`astro.h`): `bodies[10]` (Sun..Pluto, each a sign +
|
||||||
|
degree-in-sign + raw ecliptic longitude), `ascendant_longitude`,
|
||||||
|
`houses[12]` (whole-sign: `houses[0]` is the Ascendant's sign).
|
||||||
|
- **`DailyTransits`** (`astro.h`): `bodies[10]` (today's positions, same
|
||||||
|
shape as above), `moon_phase` (one of 8 named phases), `aspects[]` +
|
||||||
|
`aspect_count` (each aspect names a transiting planet, a natal planet,
|
||||||
|
an `AspectType` — conjunction/sextile/square/trine/opposition — and
|
||||||
|
the orb in degrees).
|
||||||
|
- **`CelticCrossSpread`** (`tarot.h`): `positions[10]`, indexed by
|
||||||
|
`CelticCrossPosition` (Present, Challenge, Crown, Foundation, Recent
|
||||||
|
Past, Near Future, Attitude, Environment, Hopes and Fears, Outcome —
|
||||||
|
Waite's own drawing order), each holding a `TarotCard` and a `reversed`
|
||||||
|
bool.
|
||||||
|
|
||||||
|
### `--format text`
|
||||||
|
|
||||||
|
Four `=====`-delimited sections, in order: natal chart (each body's sign
|
||||||
|
+ degree, then the Ascendant), today's sky (Moon phase, then each body's
|
||||||
|
transiting position), today's aspects to the natal chart (one line per
|
||||||
|
aspect, with orb), and the Celtic Cross (one block per position: name,
|
||||||
|
card, `(Reversed)` if applicable, the position's meaning, then the
|
||||||
|
card's meaning).
|
||||||
|
|
||||||
|
```
|
||||||
|
===== Natal Chart =====
|
||||||
|
Sun 23.5° Taurus
|
||||||
|
Moon 15.2° Capricorn
|
||||||
|
...
|
||||||
|
Ascendant 18.4° Pisces
|
||||||
|
|
||||||
|
===== Today's Sky =====
|
||||||
|
Moon phase: Waning Gibbous
|
||||||
|
Sun 11.5° Cancer
|
||||||
|
...
|
||||||
|
|
||||||
|
===== Today's Aspects to Natal Chart =====
|
||||||
|
Transiting Sun Opposition natal Moon (orb 3.7°)
|
||||||
|
...
|
||||||
|
|
||||||
|
===== Celtic Cross =====
|
||||||
|
The Present The High Priestess (Reversed)
|
||||||
|
This covers him: the general influence affecting the matter.
|
||||||
|
Passion, moral or physical ardour, conceit, surface knowledge.
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `--format html`
|
||||||
|
|
||||||
|
A single self-contained HTML page (inline `<style>`, no JS, no external
|
||||||
|
requests) with the same four sections, plus each of the 10 Celtic Cross
|
||||||
|
cards rendered as an image:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<img class="reversed" src="img/RWS_Tarot_02_High_Priestess.jpeg" alt="The High Priestess">
|
||||||
|
```
|
||||||
|
|
||||||
|
- `src="img/<file>"` is relative to the **HTML file's own location** —
|
||||||
|
it expects to sit next to an `img/` directory, exactly like
|
||||||
|
`dist/reading.html` next to `dist/img/` (which `make images` populates
|
||||||
|
from `res/img/`). Saving the HTML output anywhere else breaks the
|
||||||
|
image links.
|
||||||
|
- Reversed cards get `class="reversed"`, styled with `transform:
|
||||||
|
rotate(180deg)` in the page's inline CSS.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -std=c99 -Wall -Wextra -O2
|
||||||
|
LDLIBS := -lm
|
||||||
|
|
||||||
|
SRC_DIR := src
|
||||||
|
THIRD_PARTY := third_party/astronomy/astronomy.c
|
||||||
|
TEST_DIR := tests
|
||||||
|
BUILD_DIR := build
|
||||||
|
OBJ_DIR := $(BUILD_DIR)/obj
|
||||||
|
|
||||||
|
# Final build output lives in dist/, next to (not inside) engine/ and res/.
|
||||||
|
DIST_DIR := ../dist
|
||||||
|
DIST_IMG_DIR := $(DIST_DIR)/img
|
||||||
|
DIST_I18N_DIR := $(DIST_DIR)/i18n
|
||||||
|
RES_IMG_DIR := ../res/img
|
||||||
|
I18N_DIR := i18n
|
||||||
|
SCRIPTS_DIR := scripts
|
||||||
|
|
||||||
|
# Engine logic shared by the CLI and the test binary (no main()).
|
||||||
|
ENGINE_SRCS := $(SRC_DIR)/rng.c $(SRC_DIR)/tarot.c $(SRC_DIR)/tarot_data.c \
|
||||||
|
$(SRC_DIR)/astro.c $(SRC_DIR)/reading.c $(SRC_DIR)/i18n.c $(THIRD_PARTY)
|
||||||
|
ENGINE_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(ENGINE_SRCS))
|
||||||
|
|
||||||
|
MAIN_OBJ := $(OBJ_DIR)/$(SRC_DIR)/main.o
|
||||||
|
TEST_OBJ := $(OBJ_DIR)/$(TEST_DIR)/smoke_test.o
|
||||||
|
|
||||||
|
BINARY := $(DIST_DIR)/deck-engine
|
||||||
|
TEST_BINARY := $(BUILD_DIR)/smoke_test
|
||||||
|
|
||||||
|
.PHONY: all test clean images scripts i18n
|
||||||
|
|
||||||
|
all: $(BINARY) images scripts i18n
|
||||||
|
|
||||||
|
$(BINARY): $(ENGINE_OBJS) $(MAIN_OBJ)
|
||||||
|
@mkdir -p $(DIST_DIR)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
# The HTML report expects card art at img/<file> next to the binary, so
|
||||||
|
# every build refreshes a copy alongside it (cheap: 22 small JPEGs).
|
||||||
|
images:
|
||||||
|
@mkdir -p $(DIST_IMG_DIR)
|
||||||
|
cp $(RES_IMG_DIR)/*.jpeg $(DIST_IMG_DIR)/
|
||||||
|
|
||||||
|
# The CLI expects translation files at i18n/<lang>.lang next to the
|
||||||
|
# binary (default --i18n-dir), so every build refreshes a copy alongside
|
||||||
|
# it, same as images/. Add a new language by dropping another <code>.lang
|
||||||
|
# file into engine/i18n/ - no code or Makefile change needed.
|
||||||
|
i18n:
|
||||||
|
@mkdir -p $(DIST_I18N_DIR)
|
||||||
|
cp $(I18N_DIR)/*.lang $(DIST_I18N_DIR)/
|
||||||
|
|
||||||
|
# run.sh is refreshed every build. user.properties is only seeded once
|
||||||
|
# (from the placeholder template) so a rebuild never clobbers the user's
|
||||||
|
# own filled-in birth data.
|
||||||
|
scripts:
|
||||||
|
@mkdir -p $(DIST_DIR)
|
||||||
|
cp $(SCRIPTS_DIR)/run.sh $(DIST_DIR)/run.sh
|
||||||
|
chmod +x $(DIST_DIR)/run.sh
|
||||||
|
test -f $(DIST_DIR)/user.properties || \
|
||||||
|
cp $(SCRIPTS_DIR)/user.properties.template $(DIST_DIR)/user.properties
|
||||||
|
|
||||||
|
test: $(TEST_BINARY)
|
||||||
|
./$(TEST_BINARY)
|
||||||
|
|
||||||
|
$(TEST_BINARY): $(ENGINE_OBJS) $(TEST_OBJ)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
$(OBJ_DIR)/%.o: %.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR) $(BINARY) $(DIST_IMG_DIR) $(DIST_I18N_DIR) $(DIST_DIR)/run.sh
|
||||||
|
# user.properties holds the user's own data - never removed by clean.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Deck in a Dash -- Deutsche (de) Uebersetzung
|
||||||
|
#
|
||||||
|
# Format: key=value, siehe engine/i18n/en.lang fuer eine ausfuehrliche
|
||||||
|
# Erklaerung. Diese Uebersetzung ist eine eigene, sinngemaesse
|
||||||
|
# Uebertragung der gemeinfreien (public domain) englischen Originaltexte
|
||||||
|
# aus A. E. Waites "The Pictorial Key to the Tarot" (1911) - keine
|
||||||
|
# Uebernahme aus einer bestimmten veroeffentlichten deutschen Ausgabe.
|
||||||
|
|
||||||
|
# --- UI labels (reading.c) ---
|
||||||
|
ui.page_title=Deck in a Dash - Tägliche Deutung
|
||||||
|
ui.daily_reading=Tägliche Deutung
|
||||||
|
ui.natal_chart=Geburtshoroskop
|
||||||
|
ui.todays_sky=Heutiger Himmel
|
||||||
|
ui.todays_aspects=Heutige Aspekte zum Geburtshoroskop
|
||||||
|
ui.celtic_cross=Keltisches Kreuz
|
||||||
|
ui.ascendant=Aszendent
|
||||||
|
ui.moon_phase=Mondphase:
|
||||||
|
ui.no_aspects=(keine im Orbis)
|
||||||
|
ui.reversed=(Umgekehrt)
|
||||||
|
ui.transiting=Transit
|
||||||
|
ui.natal=natal
|
||||||
|
ui.orb=Orbis
|
||||||
|
|
||||||
|
# --- Planeten (astro.c) ---
|
||||||
|
body.sun=Sonne
|
||||||
|
body.moon=Mond
|
||||||
|
body.mercury=Merkur
|
||||||
|
body.venus=Venus
|
||||||
|
body.mars=Mars
|
||||||
|
body.jupiter=Jupiter
|
||||||
|
body.saturn=Saturn
|
||||||
|
body.uranus=Uranus
|
||||||
|
body.neptune=Neptun
|
||||||
|
body.pluto=Pluto
|
||||||
|
|
||||||
|
# --- Sternzeichen (astro.c) ---
|
||||||
|
sign.aries=Widder
|
||||||
|
sign.taurus=Stier
|
||||||
|
sign.gemini=Zwillinge
|
||||||
|
sign.cancer=Krebs
|
||||||
|
sign.leo=Löwe
|
||||||
|
sign.virgo=Jungfrau
|
||||||
|
sign.libra=Waage
|
||||||
|
sign.scorpio=Skorpion
|
||||||
|
sign.sagittarius=Schütze
|
||||||
|
sign.capricorn=Steinbock
|
||||||
|
sign.aquarius=Wassermann
|
||||||
|
sign.pisces=Fische
|
||||||
|
|
||||||
|
# --- Mondphasen (astro.c) ---
|
||||||
|
moonphase.new=Neumond
|
||||||
|
moonphase.waxing_crescent=Zunehmende Sichel
|
||||||
|
moonphase.first_quarter=Erstes Viertel
|
||||||
|
moonphase.waxing_gibbous=Zunehmender Mond
|
||||||
|
moonphase.full=Vollmond
|
||||||
|
moonphase.waning_gibbous=Abnehmender Mond
|
||||||
|
moonphase.last_quarter=Letztes Viertel
|
||||||
|
moonphase.waning_crescent=Abnehmende Sichel
|
||||||
|
|
||||||
|
# --- Aspekte (astro.c) ---
|
||||||
|
aspect.conjunction=Konjunktion
|
||||||
|
aspect.sextile=Sextil
|
||||||
|
aspect.square=Quadrat
|
||||||
|
aspect.trine=Trigon
|
||||||
|
aspect.opposition=Opposition
|
||||||
|
|
||||||
|
# --- Positionen des Keltischen Kreuzes (tarot_data.c) ---
|
||||||
|
position.present.name=Die Gegenwart
|
||||||
|
position.present.desc=Dies bedeckt ihn: der allgemeine Einfluss, der die Angelegenheit betrifft.
|
||||||
|
position.challenge.name=Die Herausforderung
|
||||||
|
position.challenge.desc=Dies kreuzt ihn: die Art des Hindernisses in der Angelegenheit.
|
||||||
|
position.crown.name=Die Krone
|
||||||
|
position.crown.desc=Dies krönt ihn: das Ziel oder Ideal, das Beste, was erreicht werden kann.
|
||||||
|
position.foundation.name=Das Fundament
|
||||||
|
position.foundation.desc=Dies liegt unter ihm: die Grundlage der Angelegenheit, bereits Wirklichkeit.
|
||||||
|
position.recent_past.name=Die jüngste Vergangenheit
|
||||||
|
position.recent_past.desc=Dies liegt hinter ihm: der Einfluss, der gerade vergeht.
|
||||||
|
position.near_future.name=Die nahe Zukunft
|
||||||
|
position.near_future.desc=Dies liegt vor ihm: der Einfluss, der jetzt wirksam wird.
|
||||||
|
position.attitude.name=Er selbst
|
||||||
|
position.attitude.desc=Seine Haltung oder Einstellung in den gegebenen Umständen.
|
||||||
|
position.environment.name=Sein Haus
|
||||||
|
position.environment.desc=Sein Umfeld und die darin wirkenden Tendenzen.
|
||||||
|
position.hopes_and_fears.name=Hoffnungen und Ängste
|
||||||
|
position.hopes_and_fears.desc=Seine Hoffnungen oder Ängste in der Angelegenheit.
|
||||||
|
position.outcome.name=Das Ergebnis
|
||||||
|
position.outcome.desc=Was kommen wird: das endgültige Ergebnis der Angelegenheit.
|
||||||
|
|
||||||
|
# --- Tarotkarten (tarot_data.c) ---
|
||||||
|
card.fool.name=Der Narr
|
||||||
|
card.fool.upright=Torheit, Manie, Extravaganz, Rausch, Delirium, Raserei.
|
||||||
|
card.fool.reversed=Nachlässigkeit, Abwesenheit, Sorglosigkeit, Apathie, Nichtigkeit, Eitelkeit.
|
||||||
|
card.magician.name=Der Magier
|
||||||
|
card.magician.upright=Geschick, Diplomatie, Gewandtheit, Feinsinn; Selbstvertrauen, Wille.
|
||||||
|
card.magician.reversed=Arzt, Magus, Geisteskrankheit, Schande, Unruhe.
|
||||||
|
card.high_priestess.name=Die Hohepriesterin
|
||||||
|
card.high_priestess.upright=Geheimnisse, Mysterium, die noch unenthüllte Zukunft; Schweigen, Beharrlichkeit, Weisheit, Wissenschaft.
|
||||||
|
card.high_priestess.reversed=Leidenschaft, moralische oder körperliche Glut, Eitelkeit, oberflächliches Wissen.
|
||||||
|
card.empress.name=Die Herrscherin
|
||||||
|
card.empress.upright=Fruchtbarkeit, Handeln, Initiative, langes Leben; auch Schwierigkeit, Zweifel, Unwissenheit.
|
||||||
|
card.empress.reversed=Licht, Wahrheit, die Klärung verworrener Angelegenheiten, öffentliche Freude; Unentschlossenheit.
|
||||||
|
card.emperor.name=Der Herrscher
|
||||||
|
card.emperor.upright=Stabilität, Macht, Schutz, Verwirklichung; Hilfe, Vernunft, Überzeugung, Autorität und Wille.
|
||||||
|
card.emperor.reversed=Wohlwollen, Mitgefühl, Ansehen; auch Verwirrung der Feinde, Behinderung, Unreife.
|
||||||
|
card.hierophant.name=Der Hierophant
|
||||||
|
card.hierophant.upright=Heirat, Bündnis, Gefangenschaft, Dienstbarkeit; nach anderer Lesart Barmherzigkeit, Güte, Inspiration.
|
||||||
|
card.hierophant.reversed=Gesellschaft, gutes Einvernehmen, Eintracht, übertriebene Güte, Schwäche.
|
||||||
|
card.lovers.name=Die Liebenden
|
||||||
|
card.lovers.upright=Anziehung, Liebe, Schönheit, überwundene Prüfungen.
|
||||||
|
card.lovers.reversed=Scheitern, törichte Pläne; vereitelte Heirat, Widrigkeiten aller Art.
|
||||||
|
card.chariot.name=Der Wagen
|
||||||
|
card.chariot.upright=Beistand, Vorsehung; auch Krieg, Triumph, Anmaßung, Rache, Unruhe.
|
||||||
|
card.chariot.reversed=Aufruhr, Streit, Zwist, Rechtsstreit, Niederlage.
|
||||||
|
card.strength.name=Kraft
|
||||||
|
card.strength.upright=Macht, Energie, Tatkraft, Mut, Großmut; voller Erfolg und Ehren.
|
||||||
|
card.strength.reversed=Despotismus, Machtmissbrauch, Schwäche, Zwietracht, mitunter sogar Schande.
|
||||||
|
card.hermit.name=Der Eremit
|
||||||
|
card.hermit.upright=Klugheit, Umsicht; auch, und besonders, Verrat, Verstellung, Gaunerei, Verderbtheit.
|
||||||
|
card.hermit.reversed=Verheimlichung, Verkleidung, Berechnung, Furcht, unbegründete Vorsicht.
|
||||||
|
card.wheel_of_fortune.name=Rad des Schicksals
|
||||||
|
card.wheel_of_fortune.upright=Schicksal, Glück, Erfolg, Aufstieg, Glücksfall, Glückseligkeit.
|
||||||
|
card.wheel_of_fortune.reversed=Zunahme, Überfluss, Übermaß.
|
||||||
|
card.justice.name=Gerechtigkeit
|
||||||
|
card.justice.upright=Gerechtigkeit, Rechtschaffenheit, Redlichkeit, Vollstreckung; Sieg der verdienten Seite vor Gericht.
|
||||||
|
card.justice.reversed=Das Recht in all seinen Bereichen, rechtliche Verwicklungen, Bigotterie, Voreingenommenheit, übertriebene Strenge.
|
||||||
|
card.hanged_man.name=Der Gehängte
|
||||||
|
card.hanged_man.upright=Weisheit, Umsicht, Urteilsvermögen, Prüfungen, Opfer, Intuition, Wahrsagung, Prophezeiung.
|
||||||
|
card.hanged_man.reversed=Selbstsucht, die Masse, das Staatswesen.
|
||||||
|
card.death.name=Der Tod
|
||||||
|
card.death.upright=Ende, Sterblichkeit, Zerstörung, Verderbnis; Verlust eines Wohltäters oder vielerlei Widrigkeiten.
|
||||||
|
card.death.reversed=Trägheit, Schlaf, Lethargie, Erstarrung, Schlafwandel; zerstörte Hoffnung.
|
||||||
|
card.temperance.name=Mäßigkeit
|
||||||
|
card.temperance.upright=Sparsamkeit, Mäßigung, Genügsamkeit, Haushalten, Anpassung.
|
||||||
|
card.temperance.reversed=Dinge, die mit Kirchen, Religionen, Sekten, dem Priestertum zusammenhängen; Zwietracht, widerstreitende Interessen.
|
||||||
|
card.devil.name=Der Teufel
|
||||||
|
card.devil.upright=Verwüstung, Gewalt, Vehemenz, außerordentliche Anstrengungen, Kraft, Verhängnis.
|
||||||
|
card.devil.reversed=Böses Verhängnis, Schwäche, Kleinlichkeit, Blindheit.
|
||||||
|
card.tower.name=Der Turm
|
||||||
|
card.tower.upright=Elend, Not, Armut, Widrigkeit, Unglück, Schande, Täuschung, Ruin; unvorhergesehene Katastrophe.
|
||||||
|
card.tower.reversed=Dasselbe in geringerem Maße; auch Unterdrückung, Gefangenschaft, Tyrannei.
|
||||||
|
card.star.name=Der Stern
|
||||||
|
card.star.upright=Verlust, Diebstahl, Entbehrung, Verlassenheit; nach anderer Lesart Hoffnung und glänzende Aussichten.
|
||||||
|
card.star.reversed=Arroganz, Hochmut, Ohnmacht.
|
||||||
|
card.moon.name=Der Mond
|
||||||
|
card.moon.upright=Verborgene Feinde, Gefahr, Verleumdung, Dunkelheit, Schrecken, Täuschung, okkulte Kräfte, Irrtum.
|
||||||
|
card.moon.reversed=Instabilität, Unbeständigkeit, Schweigen, geringere Grade von Täuschung und Irrtum.
|
||||||
|
card.sun.name=Die Sonne
|
||||||
|
card.sun.upright=Materielles Glück, glückliche Ehe, Zufriedenheit.
|
||||||
|
card.sun.reversed=Dasselbe in geringerem Maße.
|
||||||
|
card.judgement.name=Das Gericht
|
||||||
|
card.judgement.upright=Positionswechsel, Erneuerung, Ergebnis; nach anderer Lesart vollständiger Verlust durch einen Rechtsstreit.
|
||||||
|
card.judgement.reversed=Schwäche, Kleinmut, Einfalt; auch Überlegung, Entscheidung, Urteil.
|
||||||
|
card.world.name=Die Welt
|
||||||
|
card.world.upright=Gesicherter Erfolg, Belohnung, Reise, Weg, Auswanderung, Flucht, Ortswechsel.
|
||||||
|
card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständigkeit.
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# Deck in a Dash -- English (en) translations
|
||||||
|
#
|
||||||
|
# Format: key=value, one per line. "#" starts a comment, blank lines are
|
||||||
|
# ignored, leading/trailing whitespace around key and value is trimmed
|
||||||
|
# (see engine/src/i18n.c). This file also *is* the built-in fallback
|
||||||
|
# text hardcoded in the C source, kept here so it can be edited without
|
||||||
|
# recompiling and so it doubles as the template for new languages: copy
|
||||||
|
# it to <code>.lang, translate every value, keep every key identical.
|
||||||
|
#
|
||||||
|
# To add a language: copy this file to e.g. fr.lang in this directory,
|
||||||
|
# translate the values, then `make` (it copies engine/i18n/*.lang to
|
||||||
|
# dist/i18n/) and run with --lang fr (or set lang=fr in user.properties
|
||||||
|
# for run.sh). Missing keys/files silently fall back to English, so a
|
||||||
|
# translation can be filled in gradually.
|
||||||
|
|
||||||
|
# --- UI labels (reading.c) ---
|
||||||
|
ui.page_title=Deck in a Dash - Daily Reading
|
||||||
|
ui.daily_reading=Daily Reading
|
||||||
|
ui.natal_chart=Natal Chart
|
||||||
|
ui.todays_sky=Today's Sky
|
||||||
|
ui.todays_aspects=Today's Aspects to Natal Chart
|
||||||
|
ui.celtic_cross=Celtic Cross
|
||||||
|
ui.ascendant=Ascendant
|
||||||
|
ui.moon_phase=Moon phase:
|
||||||
|
ui.no_aspects=(none within orb)
|
||||||
|
ui.reversed=(Reversed)
|
||||||
|
ui.transiting=Transiting
|
||||||
|
ui.natal=natal
|
||||||
|
ui.orb=orb
|
||||||
|
|
||||||
|
# --- Planets/luminaries (astro.c) ---
|
||||||
|
body.sun=Sun
|
||||||
|
body.moon=Moon
|
||||||
|
body.mercury=Mercury
|
||||||
|
body.venus=Venus
|
||||||
|
body.mars=Mars
|
||||||
|
body.jupiter=Jupiter
|
||||||
|
body.saturn=Saturn
|
||||||
|
body.uranus=Uranus
|
||||||
|
body.neptune=Neptune
|
||||||
|
body.pluto=Pluto
|
||||||
|
|
||||||
|
# --- Zodiac signs (astro.c) ---
|
||||||
|
sign.aries=Aries
|
||||||
|
sign.taurus=Taurus
|
||||||
|
sign.gemini=Gemini
|
||||||
|
sign.cancer=Cancer
|
||||||
|
sign.leo=Leo
|
||||||
|
sign.virgo=Virgo
|
||||||
|
sign.libra=Libra
|
||||||
|
sign.scorpio=Scorpio
|
||||||
|
sign.sagittarius=Sagittarius
|
||||||
|
sign.capricorn=Capricorn
|
||||||
|
sign.aquarius=Aquarius
|
||||||
|
sign.pisces=Pisces
|
||||||
|
|
||||||
|
# --- Moon phases (astro.c) ---
|
||||||
|
moonphase.new=New Moon
|
||||||
|
moonphase.waxing_crescent=Waxing Crescent
|
||||||
|
moonphase.first_quarter=First Quarter
|
||||||
|
moonphase.waxing_gibbous=Waxing Gibbous
|
||||||
|
moonphase.full=Full Moon
|
||||||
|
moonphase.waning_gibbous=Waning Gibbous
|
||||||
|
moonphase.last_quarter=Last Quarter
|
||||||
|
moonphase.waning_crescent=Waning Crescent
|
||||||
|
|
||||||
|
# --- Aspects (astro.c) ---
|
||||||
|
aspect.conjunction=Conjunction
|
||||||
|
aspect.sextile=Sextile
|
||||||
|
aspect.square=Square
|
||||||
|
aspect.trine=Trine
|
||||||
|
aspect.opposition=Opposition
|
||||||
|
|
||||||
|
# --- Celtic Cross position names/descriptions (tarot_data.c), Waite's
|
||||||
|
# own wording from "An Ancient Celtic Method of Divination" ---
|
||||||
|
position.present.name=The Present
|
||||||
|
position.present.desc=This covers him: the general influence affecting the matter.
|
||||||
|
position.challenge.name=The Challenge
|
||||||
|
position.challenge.desc=This crosses him: the nature of the obstacle in the matter.
|
||||||
|
position.crown.name=The Crown
|
||||||
|
position.crown.desc=This crowns him: the aim or ideal, the best that can be achieved.
|
||||||
|
position.foundation.name=The Foundation
|
||||||
|
position.foundation.desc=This is beneath him: the basis of the matter, already actual.
|
||||||
|
position.recent_past.name=The Recent Past
|
||||||
|
position.recent_past.desc=This is behind him: the influence that is just passing away.
|
||||||
|
position.near_future.name=The Near Future
|
||||||
|
position.near_future.desc=This is before him: the influence now coming into action.
|
||||||
|
position.attitude.name=Himself
|
||||||
|
position.attitude.desc=His position or attitude in the circumstances.
|
||||||
|
position.environment.name=His House
|
||||||
|
position.environment.desc=His environment and the tendencies at work therein.
|
||||||
|
position.hopes_and_fears.name=Hopes and Fears
|
||||||
|
position.hopes_and_fears.desc=His hopes or fears in the matter.
|
||||||
|
position.outcome.name=The Outcome
|
||||||
|
position.outcome.desc=What will come: the final result of the matter.
|
||||||
|
|
||||||
|
# --- Tarot cards (tarot_data.c), condensed from A. E. Waite's The
|
||||||
|
# Pictorial Key to the Tarot (1911), Part III §3 (public domain) ---
|
||||||
|
card.fool.name=The Fool
|
||||||
|
card.fool.upright=Folly, mania, extravagance, intoxication, delirium, frenzy.
|
||||||
|
card.fool.reversed=Negligence, absence, carelessness, apathy, nullity, vanity.
|
||||||
|
card.magician.name=The Magician
|
||||||
|
card.magician.upright=Skill, diplomacy, address, subtlety; self-confidence, will.
|
||||||
|
card.magician.reversed=Physician, Magus, mental disease, disgrace, disquiet.
|
||||||
|
card.high_priestess.name=The High Priestess
|
||||||
|
card.high_priestess.upright=Secrets, mystery, the future as yet unrevealed; silence, tenacity, wisdom, science.
|
||||||
|
card.high_priestess.reversed=Passion, moral or physical ardour, conceit, surface knowledge.
|
||||||
|
card.empress.name=The Empress
|
||||||
|
card.empress.upright=Fruitfulness, action, initiative, length of days; also difficulty, doubt, ignorance.
|
||||||
|
card.empress.reversed=Light, truth, the unravelling of involved matters, public rejoicings; vacillation.
|
||||||
|
card.emperor.name=The Emperor
|
||||||
|
card.emperor.upright=Stability, power, protection, realization; aid, reason, conviction, authority and will.
|
||||||
|
card.emperor.reversed=Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.
|
||||||
|
card.hierophant.name=The Hierophant
|
||||||
|
card.hierophant.upright=Marriage, alliance, captivity, servitude; by another account, mercy, goodness, inspiration.
|
||||||
|
card.hierophant.reversed=Society, good understanding, concord, over-kindness, weakness.
|
||||||
|
card.lovers.name=The Lovers
|
||||||
|
card.lovers.upright=Attraction, love, beauty, trials overcome.
|
||||||
|
card.lovers.reversed=Failure, foolish designs; marriage frustrated, contrarieties of all kinds.
|
||||||
|
card.chariot.name=The Chariot
|
||||||
|
card.chariot.upright=Succour, providence; also war, triumph, presumption, vengeance, trouble.
|
||||||
|
card.chariot.reversed=Riot, quarrel, dispute, litigation, defeat.
|
||||||
|
card.strength.name=Strength
|
||||||
|
card.strength.upright=Power, energy, action, courage, magnanimity; complete success and honours.
|
||||||
|
card.strength.reversed=Despotism, abuse of power, weakness, discord, sometimes even disgrace.
|
||||||
|
card.hermit.name=The Hermit
|
||||||
|
card.hermit.upright=Prudence, circumspection; also, and especially, treason, dissimulation, roguery, corruption.
|
||||||
|
card.hermit.reversed=Concealment, disguise, policy, fear, unreasoned caution.
|
||||||
|
card.wheel_of_fortune.name=Wheel of Fortune
|
||||||
|
card.wheel_of_fortune.upright=Destiny, fortune, success, elevation, luck, felicity.
|
||||||
|
card.wheel_of_fortune.reversed=Increase, abundance, superfluity.
|
||||||
|
card.justice.name=Justice
|
||||||
|
card.justice.upright=Equity, rightness, probity, executive; triumph of the deserving side in law.
|
||||||
|
card.justice.reversed=Law in all its departments, legal complications, bigotry, bias, excessive severity.
|
||||||
|
card.hanged_man.name=The Hanged Man
|
||||||
|
card.hanged_man.upright=Wisdom, circumspection, discernment, trials, sacrifice, intuition, divination, prophecy.
|
||||||
|
card.hanged_man.reversed=Selfishness, the crowd, body politic.
|
||||||
|
card.death.name=Death
|
||||||
|
card.death.upright=End, mortality, destruction, corruption; loss of a benefactor, or many contrarieties.
|
||||||
|
card.death.reversed=Inertia, sleep, lethargy, petrifaction, somnambulism; hope destroyed.
|
||||||
|
card.temperance.name=Temperance
|
||||||
|
card.temperance.upright=Economy, moderation, frugality, management, accommodation.
|
||||||
|
card.temperance.reversed=Things connected with churches, religions, sects, the priesthood; disunion, competing interests.
|
||||||
|
card.devil.name=The Devil
|
||||||
|
card.devil.upright=Ravage, violence, vehemence, extraordinary efforts, force, fatality.
|
||||||
|
card.devil.reversed=Evil fatality, weakness, pettiness, blindness.
|
||||||
|
card.tower.name=The Tower
|
||||||
|
card.tower.upright=Misery, distress, indigence, adversity, calamity, disgrace, deception, ruin; unforeseen catastrophe.
|
||||||
|
card.tower.reversed=The same in a lesser degree; also oppression, imprisonment, tyranny.
|
||||||
|
card.star.name=The Star
|
||||||
|
card.star.upright=Loss, theft, privation, abandonment; another reading says hope and bright prospects.
|
||||||
|
card.star.reversed=Arrogance, haughtiness, impotence.
|
||||||
|
card.moon.name=The Moon
|
||||||
|
card.moon.upright=Hidden enemies, danger, calumny, darkness, terror, deception, occult forces, error.
|
||||||
|
card.moon.reversed=Instability, inconstancy, silence, lesser degrees of deception and error.
|
||||||
|
card.sun.name=The Sun
|
||||||
|
card.sun.upright=Material happiness, fortunate marriage, contentment.
|
||||||
|
card.sun.reversed=The same in a lesser sense.
|
||||||
|
card.judgement.name=Judgement
|
||||||
|
card.judgement.upright=Change of position, renewal, outcome; another account specifies total loss through lawsuit.
|
||||||
|
card.judgement.reversed=Weakness, pusillanimity, simplicity; also deliberation, decision, sentence.
|
||||||
|
card.world.name=The World
|
||||||
|
card.world.upright=Assured success, recompense, voyage, route, emigration, flight, change of place.
|
||||||
|
card.world.reversed=Inertia, fixity, stagnation, permanence.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Runs deck-engine for "today": the tarot seed and the transit moment come
|
||||||
|
# from the OS clock, birth data comes from user.properties next to this
|
||||||
|
# script. Usage: ./run.sh [text|html] [lang]
|
||||||
|
#
|
||||||
|
# [lang] overrides user.properties' lang= for this run only (e.g.
|
||||||
|
# ./run.sh html de); omit it to use lang= from user.properties, which
|
||||||
|
# itself defaults to "en" if unset.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BINARY="$SCRIPT_DIR/deck-engine"
|
||||||
|
PROPERTIES_FILE="$SCRIPT_DIR/user.properties"
|
||||||
|
|
||||||
|
if [ ! -x "$BINARY" ]; then
|
||||||
|
echo "error: $BINARY not found or not executable (run 'make' in engine/ first)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$PROPERTIES_FILE" ]; then
|
||||||
|
echo "error: $PROPERTIES_FILE not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
birth_date=""
|
||||||
|
birth_time=""
|
||||||
|
birth_utc_offset=""
|
||||||
|
birth_lat=""
|
||||||
|
birth_lon=""
|
||||||
|
lang="en"
|
||||||
|
|
||||||
|
while IFS='=' read -r key value || [ -n "$key" ]; do
|
||||||
|
key="$(printf '%s' "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||||
|
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||||
|
case "$key" in
|
||||||
|
'' | '#'*) continue ;;
|
||||||
|
esac
|
||||||
|
case "$key" in
|
||||||
|
birth_date) birth_date="$value" ;;
|
||||||
|
birth_time) birth_time="$value" ;;
|
||||||
|
birth_utc_offset) birth_utc_offset="$value" ;;
|
||||||
|
birth_lat) birth_lat="$value" ;;
|
||||||
|
birth_lon) birth_lon="$value" ;;
|
||||||
|
lang) [ -n "$value" ] && lang="$value" ;;
|
||||||
|
esac
|
||||||
|
done <"$PROPERTIES_FILE"
|
||||||
|
|
||||||
|
check_set() {
|
||||||
|
case "$2" in
|
||||||
|
'' | *YOUR_*)
|
||||||
|
echo "error: $PROPERTIES_FILE: '$1' is not set - edit the file and fill in your details." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
check_set birth_date "$birth_date"
|
||||||
|
check_set birth_time "$birth_time"
|
||||||
|
check_set birth_utc_offset "$birth_utc_offset"
|
||||||
|
check_set birth_lat "$birth_lat"
|
||||||
|
check_set birth_lon "$birth_lon"
|
||||||
|
|
||||||
|
# OS-provided inputs: today's date is the tarot seed (stable all day, a
|
||||||
|
# new spread each day), current UTC time drives today's transits.
|
||||||
|
seed="$(date +%Y-%m-%d)"
|
||||||
|
transit_date="$(date -u +%Y-%m-%dT%H:%M)"
|
||||||
|
format="${1:-text}"
|
||||||
|
lang="${2:-$lang}"
|
||||||
|
|
||||||
|
exec "$BINARY" \
|
||||||
|
--seed "$seed" \
|
||||||
|
--birth-date "$birth_date" \
|
||||||
|
--birth-time "$birth_time" \
|
||||||
|
--birth-utc-offset "$birth_utc_offset" \
|
||||||
|
--birth-lat "$birth_lat" \
|
||||||
|
--birth-lon "$birth_lon" \
|
||||||
|
--date "$transit_date" \
|
||||||
|
--format "$format" \
|
||||||
|
--lang "$lang" \
|
||||||
|
--i18n-dir "$SCRIPT_DIR/i18n"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# deck-in-a-dash user properties
|
||||||
|
#
|
||||||
|
# Fill in your birth details below, then run ./run.sh (or ./run.sh html).
|
||||||
|
# This file is only copied here once by the build - it will not be
|
||||||
|
# overwritten by later builds, so it's safe to edit in place.
|
||||||
|
#
|
||||||
|
# birth_date Birth date, local calendar, YYYY-MM-DD
|
||||||
|
# birth_time Birth time, local clock, 24h HH:MM
|
||||||
|
# birth_utc_offset Hours to ADD to UTC to get your birth's local time
|
||||||
|
# (e.g. 2 for Central European Summer Time)
|
||||||
|
# birth_lat Birth location latitude, decimal degrees, north positive
|
||||||
|
# birth_lon Birth location longitude, decimal degrees, east positive
|
||||||
|
# lang Reading language code - matches a file named
|
||||||
|
# <code>.lang in dist/i18n/ (see engine/i18n/ for the
|
||||||
|
# source files, currently "en" and "de"). Optional,
|
||||||
|
# defaults to "en"; unknown codes silently fall back
|
||||||
|
# to English too.
|
||||||
|
|
||||||
|
birth_date=YOUR_BIRTH_DATE_HERE
|
||||||
|
birth_time=YOUR_BIRTH_TIME_HERE
|
||||||
|
birth_utc_offset=YOUR_UTC_OFFSET_HERE
|
||||||
|
birth_lat=YOUR_LATITUDE_HERE
|
||||||
|
birth_lon=YOUR_LONGITUDE_HERE
|
||||||
|
lang=en
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L /* for gmtime_r */
|
||||||
|
|
||||||
|
#include "astro.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
#include "../third_party/astronomy/astronomy.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* DEG2RAD / RAD2DEG come from astronomy.h. */
|
||||||
|
|
||||||
|
static const astro_body_t k_astro_body[NUM_BODIES] = {
|
||||||
|
[PLANET_SUN] = BODY_SUN,
|
||||||
|
[PLANET_MOON] = BODY_MOON,
|
||||||
|
[PLANET_MERCURY] = BODY_MERCURY,
|
||||||
|
[PLANET_VENUS] = BODY_VENUS,
|
||||||
|
[PLANET_MARS] = BODY_MARS,
|
||||||
|
[PLANET_JUPITER] = BODY_JUPITER,
|
||||||
|
[PLANET_SATURN] = BODY_SATURN,
|
||||||
|
[PLANET_URANUS] = BODY_URANUS,
|
||||||
|
[PLANET_NEPTUNE] = BODY_NEPTUNE,
|
||||||
|
[PLANET_PLUTO] = BODY_PLUTO,
|
||||||
|
};
|
||||||
|
|
||||||
|
static double normalize_degrees(double deg) {
|
||||||
|
double d = fmod(deg, 360.0);
|
||||||
|
return d < 0.0 ? d + 360.0 : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void longitude_to_position(double longitude, PlanetPosition *out) {
|
||||||
|
out->ecliptic_longitude = normalize_degrees(longitude);
|
||||||
|
out->sign = (ZodiacSign)((int)(out->ecliptic_longitude / 30.0) % 12);
|
||||||
|
out->degree_in_sign = fmod(out->ecliptic_longitude, 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mean obliquity of the ecliptic (IAU low-precision polynomial), matching
|
||||||
|
* what Astronomy Engine computes internally in its (non-exported)
|
||||||
|
* mean_obliq(). T is Julian centuries since J2000.0. */
|
||||||
|
static double mean_obliquity_deg(double t_centuries) {
|
||||||
|
double t = t_centuries;
|
||||||
|
return 23.4392911 - 0.0130042 * t - 0.00000016 * t * t + 0.000000504 * t * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ascendant (rising ecliptic degree), via the standard RAMC/obliquity/
|
||||||
|
* latitude identity (Duffett-Smith & Zwart, "Practical Astronomy with
|
||||||
|
* your Calculator or Spreadsheet"). Astronomy Engine has no built-in
|
||||||
|
* Ascendant function. */
|
||||||
|
static double compute_ascendant(astro_time_t *time, double latitude_deg,
|
||||||
|
double longitude_deg) {
|
||||||
|
double gast_hours = Astronomy_SiderealTime(time);
|
||||||
|
double ramc_deg = normalize_degrees(gast_hours * 15.0 + longitude_deg);
|
||||||
|
double eps_deg = mean_obliquity_deg(time->tt / 36525.0);
|
||||||
|
|
||||||
|
double ramc = ramc_deg * DEG2RAD;
|
||||||
|
double eps = eps_deg * DEG2RAD;
|
||||||
|
double lat = latitude_deg * DEG2RAD;
|
||||||
|
|
||||||
|
double y = -cos(ramc);
|
||||||
|
double x = sin(ramc) * cos(eps) + tan(lat) * sin(eps);
|
||||||
|
return normalize_degrees(atan2(y, x) * RAD2DEG);
|
||||||
|
}
|
||||||
|
|
||||||
|
static astro_time_t time_from_birth(const BirthData *birth) {
|
||||||
|
astro_time_t local = Astronomy_MakeTime(birth->year, birth->month, birth->day,
|
||||||
|
birth->hour, birth->minute, 0.0);
|
||||||
|
return Astronomy_AddDays(local, -birth->utc_offset_hours / 24.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static astro_time_t time_from_unix(time_t utc_moment) {
|
||||||
|
struct tm tm_utc;
|
||||||
|
gmtime_r(&utc_moment, &tm_utc);
|
||||||
|
return Astronomy_MakeTime(tm_utc.tm_year + 1900, tm_utc.tm_mon + 1, tm_utc.tm_mday,
|
||||||
|
tm_utc.tm_hour, tm_utc.tm_min, (double)tm_utc.tm_sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BODIES]) {
|
||||||
|
/* Astronomy_EclipticLongitude() computes *heliocentric* longitude (and
|
||||||
|
* outright rejects BODY_SUN) - wrong for astrology, which needs the
|
||||||
|
* apparent geocentric position. Astronomy_GeoVector() + Astronomy_Ecliptic()
|
||||||
|
* gives that for every body, Sun included. */
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
astro_vector_t geo = Astronomy_GeoVector(k_astro_body[b], time, ABERRATION);
|
||||||
|
astro_ecliptic_t eclip = Astronomy_Ecliptic(geo);
|
||||||
|
longitude_to_position(eclip.elon, &out[b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
|
||||||
|
astro_time_t time = time_from_birth(birth);
|
||||||
|
|
||||||
|
compute_body_positions(time, out->bodies);
|
||||||
|
out->ascendant_longitude = compute_ascendant(&time, birth->latitude, birth->longitude);
|
||||||
|
|
||||||
|
int ascendant_sign = (int)(out->ascendant_longitude / 30.0) % 12;
|
||||||
|
for (int house = 0; house < 12; house++) {
|
||||||
|
out->houses[house] = (ZodiacSign)((ascendant_sign + house) % 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static MoonPhaseName moon_phase_from_angle(double angle_deg) {
|
||||||
|
int bucket = ((int)((angle_deg + 22.5) / 45.0)) % 8;
|
||||||
|
return (MoonPhaseName)bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const double k_aspect_angle[5] = {
|
||||||
|
[ASPECT_CONJUNCTION] = 0.0,
|
||||||
|
[ASPECT_SEXTILE] = 60.0,
|
||||||
|
[ASPECT_SQUARE] = 90.0,
|
||||||
|
[ASPECT_TRINE] = 120.0,
|
||||||
|
[ASPECT_OPPOSITION] = 180.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static double angular_separation(double a, double b) {
|
||||||
|
double diff = fabs(normalize_degrees(a) - normalize_degrees(b));
|
||||||
|
return diff > 180.0 ? 360.0 - diff : diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double orb_for_pair(Body transiting, Body natal) {
|
||||||
|
/* Wider orb when a luminary (Sun or Moon) is involved, per standard
|
||||||
|
* convention. */
|
||||||
|
bool luminary = transiting == PLANET_SUN || transiting == PLANET_MOON ||
|
||||||
|
natal == PLANET_SUN || natal == PLANET_MOON;
|
||||||
|
return luminary ? 8.0 : 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
|
||||||
|
DailyTransits *out) {
|
||||||
|
astro_time_t time = time_from_unix(utc_moment);
|
||||||
|
|
||||||
|
compute_body_positions(time, out->bodies);
|
||||||
|
|
||||||
|
astro_angle_result_t phase = Astronomy_MoonPhase(time);
|
||||||
|
out->moon_phase = moon_phase_from_angle(phase.angle);
|
||||||
|
|
||||||
|
out->aspect_count = 0;
|
||||||
|
for (int t = 0; t < NUM_BODIES; t++) {
|
||||||
|
for (int n = 0; n < NUM_BODIES; n++) {
|
||||||
|
double separation = angular_separation(out->bodies[t].ecliptic_longitude,
|
||||||
|
natal->bodies[n].ecliptic_longitude);
|
||||||
|
double orb_limit = orb_for_pair((Body)t, (Body)n);
|
||||||
|
|
||||||
|
for (int a = 0; a < 5; a++) {
|
||||||
|
double orb = fabs(separation - k_aspect_angle[a]);
|
||||||
|
if (orb <= orb_limit && out->aspect_count < MAX_ASPECTS) {
|
||||||
|
Aspect *aspect = &out->aspects[out->aspect_count++];
|
||||||
|
aspect->transiting_planet = (Body)t;
|
||||||
|
aspect->natal_planet = (Body)n;
|
||||||
|
aspect->type = (AspectType)a;
|
||||||
|
aspect->orb = orb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *const k_body_names[NUM_BODIES] = {
|
||||||
|
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
||||||
|
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_sign_names[12] = {
|
||||||
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||||
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_moon_phase_names[8] = {
|
||||||
|
"New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
|
||||||
|
"Full Moon", "Waning Gibbous", "Last Quarter", "Waning Crescent",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_aspect_names[5] = {
|
||||||
|
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Slugs used to build i18n.c lookup keys - kept separate from the C enum
|
||||||
|
* names so a translation file's keys don't depend on identifiers that
|
||||||
|
* might get renamed. */
|
||||||
|
static const char *const k_body_slug[NUM_BODIES] = {
|
||||||
|
"sun", "moon", "mercury", "venus", "mars",
|
||||||
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_sign_slug[12] = {
|
||||||
|
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
||||||
|
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_moon_phase_slug[8] = {
|
||||||
|
"new", "waxing_crescent", "first_quarter", "waxing_gibbous",
|
||||||
|
"full", "waning_gibbous", "last_quarter", "waning_crescent",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_aspect_slug[5] = {
|
||||||
|
"conjunction", "sextile", "square", "trine", "opposition",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *lookup(const char *prefix, const char *slug, const char *fallback) {
|
||||||
|
char key[64];
|
||||||
|
strcpy(key, prefix);
|
||||||
|
strcat(key, slug);
|
||||||
|
return i18n_get(key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *astro_body_name(Body body) {
|
||||||
|
return lookup("body.", k_body_slug[body], k_body_names[body]);
|
||||||
|
}
|
||||||
|
const char *astro_sign_name(ZodiacSign sign) {
|
||||||
|
return lookup("sign.", k_sign_slug[sign], k_sign_names[sign]);
|
||||||
|
}
|
||||||
|
const char *astro_moon_phase_name(MoonPhaseName phase) {
|
||||||
|
return lookup("moonphase.", k_moon_phase_slug[phase], k_moon_phase_names[phase]);
|
||||||
|
}
|
||||||
|
const char *astro_aspect_name(AspectType type) {
|
||||||
|
return lookup("aspect.", k_aspect_slug[type], k_aspect_names[type]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#ifndef DECK_ASTRO_H
|
||||||
|
#define DECK_ASTRO_H
|
||||||
|
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define NUM_BODIES 10
|
||||||
|
#define MAX_ASPECTS 100
|
||||||
|
|
||||||
|
/* Named distinctly from Astronomy Engine's own astro_body_t constants
|
||||||
|
* (BODY_SUN, BODY_MOON, ...) to avoid colliding with them in astro.c,
|
||||||
|
* which includes both headers. */
|
||||||
|
typedef enum {
|
||||||
|
PLANET_SUN = 0,
|
||||||
|
PLANET_MOON,
|
||||||
|
PLANET_MERCURY,
|
||||||
|
PLANET_VENUS,
|
||||||
|
PLANET_MARS,
|
||||||
|
PLANET_JUPITER,
|
||||||
|
PLANET_SATURN,
|
||||||
|
PLANET_URANUS,
|
||||||
|
PLANET_NEPTUNE,
|
||||||
|
PLANET_PLUTO
|
||||||
|
} Body;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SIGN_ARIES = 0,
|
||||||
|
SIGN_TAURUS,
|
||||||
|
SIGN_GEMINI,
|
||||||
|
SIGN_CANCER,
|
||||||
|
SIGN_LEO,
|
||||||
|
SIGN_VIRGO,
|
||||||
|
SIGN_LIBRA,
|
||||||
|
SIGN_SCORPIO,
|
||||||
|
SIGN_SAGITTARIUS,
|
||||||
|
SIGN_CAPRICORN,
|
||||||
|
SIGN_AQUARIUS,
|
||||||
|
SIGN_PISCES
|
||||||
|
} ZodiacSign;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MOON_NEW = 0,
|
||||||
|
MOON_WAXING_CRESCENT,
|
||||||
|
MOON_FIRST_QUARTER,
|
||||||
|
MOON_WAXING_GIBBOUS,
|
||||||
|
MOON_FULL,
|
||||||
|
MOON_WANING_GIBBOUS,
|
||||||
|
MOON_LAST_QUARTER,
|
||||||
|
MOON_WANING_CRESCENT
|
||||||
|
} MoonPhaseName;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ASPECT_CONJUNCTION = 0,
|
||||||
|
ASPECT_SEXTILE,
|
||||||
|
ASPECT_SQUARE,
|
||||||
|
ASPECT_TRINE,
|
||||||
|
ASPECT_OPPOSITION
|
||||||
|
} AspectType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
double ecliptic_longitude; /* geocentric, degrees, [0, 360) */
|
||||||
|
ZodiacSign sign;
|
||||||
|
double degree_in_sign; /* [0, 30) */
|
||||||
|
} PlanetPosition;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int year, month, day; /* birth date, local calendar */
|
||||||
|
int hour, minute; /* birth time, local clock */
|
||||||
|
double utc_offset_hours; /* local time = UTC + utc_offset_hours */
|
||||||
|
double latitude; /* degrees north positive */
|
||||||
|
double longitude; /* degrees east positive */
|
||||||
|
} BirthData;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
PlanetPosition bodies[NUM_BODIES];
|
||||||
|
double ascendant_longitude;
|
||||||
|
ZodiacSign houses[12]; /* houses[0] = sign of house 1 (whole-sign system) */
|
||||||
|
} NatalChart;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Body transiting_planet;
|
||||||
|
Body natal_planet;
|
||||||
|
AspectType type;
|
||||||
|
double orb; /* how many degrees off exact, always >= 0 */
|
||||||
|
} Aspect;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
PlanetPosition bodies[NUM_BODIES];
|
||||||
|
MoonPhaseName moon_phase;
|
||||||
|
Aspect aspects[MAX_ASPECTS];
|
||||||
|
int aspect_count;
|
||||||
|
} DailyTransits;
|
||||||
|
|
||||||
|
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out);
|
||||||
|
void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
|
||||||
|
DailyTransits *out);
|
||||||
|
|
||||||
|
const char *astro_body_name(Body body);
|
||||||
|
const char *astro_sign_name(ZodiacSign sign);
|
||||||
|
const char *astro_moon_phase_name(MoonPhaseName phase);
|
||||||
|
const char *astro_aspect_name(AspectType type);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Fixed-size, no heap allocation - deliberately (see i18n.h): keeps this
|
||||||
|
* usable in a small embedded environment later, not just on desktop.
|
||||||
|
* ~140 keys are shipped today (see engine/i18n/en.lang); this leaves
|
||||||
|
* plenty of headroom for more languages/keys without growing the format. */
|
||||||
|
#define I18N_MAX_ENTRIES 320
|
||||||
|
#define I18N_MAX_KEY 48
|
||||||
|
#define I18N_MAX_VALUE 400
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char key[I18N_MAX_KEY];
|
||||||
|
char value[I18N_MAX_VALUE];
|
||||||
|
} I18nEntry;
|
||||||
|
|
||||||
|
static I18nEntry g_entries[I18N_MAX_ENTRIES];
|
||||||
|
static int g_entry_count = 0;
|
||||||
|
|
||||||
|
/* Bounded copy that's always null-terminated. Avoids strncpy() here,
|
||||||
|
* which on glibc triggers a (harmless but noisy) -Wstringop-truncation
|
||||||
|
* warning for exactly this "copy and then terminate" pattern. */
|
||||||
|
static void copy_bounded(char *dst, size_t dst_size, const char *src) {
|
||||||
|
size_t len = strlen(src);
|
||||||
|
if (len >= dst_size) len = dst_size - 1;
|
||||||
|
memcpy(dst, src, len);
|
||||||
|
dst[len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void trim(char *s) {
|
||||||
|
char *start = s;
|
||||||
|
while (*start == ' ' || *start == '\t') start++;
|
||||||
|
size_t len = strlen(start);
|
||||||
|
while (len > 0 && (start[len - 1] == ' ' || start[len - 1] == '\t')) len--;
|
||||||
|
memmove(s, start, len);
|
||||||
|
s[len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool i18n_load(const char *path) {
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return false;
|
||||||
|
|
||||||
|
g_entry_count = 0;
|
||||||
|
char line[I18N_MAX_KEY + I18N_MAX_VALUE + 4];
|
||||||
|
while (fgets(line, sizeof line, f) && g_entry_count < I18N_MAX_ENTRIES) {
|
||||||
|
line[strcspn(line, "\r\n")] = '\0';
|
||||||
|
|
||||||
|
char *first = line;
|
||||||
|
while (*first == ' ' || *first == '\t') first++;
|
||||||
|
if (*first == '\0' || *first == '#') continue;
|
||||||
|
|
||||||
|
char *eq = strchr(line, '=');
|
||||||
|
if (!eq) continue;
|
||||||
|
*eq = '\0';
|
||||||
|
char *key = line;
|
||||||
|
char *value = eq + 1;
|
||||||
|
trim(key);
|
||||||
|
trim(value);
|
||||||
|
if (key[0] == '\0') continue;
|
||||||
|
|
||||||
|
I18nEntry *entry = &g_entries[g_entry_count++];
|
||||||
|
copy_bounded(entry->key, sizeof entry->key, key);
|
||||||
|
copy_bounded(entry->value, sizeof entry->value, value);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *i18n_get(const char *key, const char *fallback) {
|
||||||
|
for (int i = 0; i < g_entry_count; i++) {
|
||||||
|
if (strcmp(g_entries[i].key, key) == 0) return g_entries[i].value;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#ifndef DECK_I18N_H
|
||||||
|
#define DECK_I18N_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Loads a `key=value` translation file (# comments and blank lines
|
||||||
|
* ignored, same format as dist/user.properties) into a process-global
|
||||||
|
* catalog, replacing whatever was loaded before. Returns false if the
|
||||||
|
* file can't be opened (the catalog is left as it was in that case).
|
||||||
|
*
|
||||||
|
* This uses stdio, so - like main.c and reading_print_text/_html - it's
|
||||||
|
* a desktop-only entry point, not something the Pebble watchapp will
|
||||||
|
* call as-is. i18n_get() below is a pure table lookup, so it's fine to
|
||||||
|
* port; the watchapp will just need its own (non-file-based) way to
|
||||||
|
* populate the catalog, e.g. from a compiled-in resource. */
|
||||||
|
bool i18n_load(const char *path);
|
||||||
|
|
||||||
|
/* Returns the translation for `key` from the loaded catalog, or
|
||||||
|
* `fallback` if no catalog is loaded or `key` isn't in it. Every caller
|
||||||
|
* in this codebase passes the built-in English text as `fallback`, so a
|
||||||
|
* missing file or a partially-translated one degrades to English rather
|
||||||
|
* than showing raw keys or blank strings. */
|
||||||
|
const char *i18n_get(const char *key, const char *fallback);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#define _DEFAULT_SOURCE /* for timegm */
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "reading.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
typedef enum { FORMAT_TEXT, FORMAT_HTML } OutputFormat;
|
||||||
|
|
||||||
|
static void print_usage(const char *prog) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM\n"
|
||||||
|
" --birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>\n"
|
||||||
|
" [--date YYYY-MM-DDTHH:MM] [--format text|html]\n"
|
||||||
|
" [--lang <code>] [--i18n-dir <path>]\n\n"
|
||||||
|
" --seed Tarot seed string; same seed -> same Celtic Cross spread.\n"
|
||||||
|
" --birth-date/time Birth date/time in local clock time.\n"
|
||||||
|
" --birth-utc-offset Hours to subtract from local birth time to get UTC (e.g. 2 for CEST).\n"
|
||||||
|
" --birth-lat/lon Birth location in decimal degrees (north/east positive).\n"
|
||||||
|
" --date UTC moment for today's transits/tarot draw. Defaults to now.\n"
|
||||||
|
" --format Output format, defaults to text. html links card art via\n"
|
||||||
|
" img/<file>, i.e. it expects to be saved next to the img/\n"
|
||||||
|
" directory that `make images` copies into dist/.\n"
|
||||||
|
" --lang Reading language code, defaults to en. Matches a file\n"
|
||||||
|
" named <code>.lang in --i18n-dir (see engine/i18n/).\n"
|
||||||
|
" --i18n-dir Directory to look up <lang>.lang in. Defaults to the\n"
|
||||||
|
" i18n/ directory next to this binary (`make i18n`\n"
|
||||||
|
" copies engine/i18n/ there as dist/i18n/).\n",
|
||||||
|
prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default --i18n-dir: <directory this binary was invoked from>/i18n, so
|
||||||
|
* `dist/deck-engine ...` finds dist/i18n/ regardless of the caller's cwd.
|
||||||
|
* If argv[0] has no '/' (looked up via $PATH), falls back to a relative
|
||||||
|
* "i18n" - callers in that situation should pass --i18n-dir explicitly. */
|
||||||
|
static void default_i18n_path(const char *argv0, const char *lang, char *out, size_t out_size) {
|
||||||
|
const char *slash = strrchr(argv0, '/');
|
||||||
|
if (slash) {
|
||||||
|
int dir_len = (int)(slash - argv0);
|
||||||
|
snprintf(out, out_size, "%.*s/i18n/%s.lang", dir_len, argv0, lang);
|
||||||
|
} else {
|
||||||
|
snprintf(out, out_size, "i18n/%s.lang", lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int require_arg(int argc, char **argv, int *i, const char *name, const char **out) {
|
||||||
|
if (*i + 1 >= argc) {
|
||||||
|
fprintf(stderr, "Missing value for %s\n", name);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*out = argv[++*i];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *seed = NULL;
|
||||||
|
const char *birth_date = NULL, *birth_time = NULL;
|
||||||
|
const char *birth_utc_offset = NULL, *birth_lat = NULL, *birth_lon = NULL;
|
||||||
|
const char *date_str = NULL, *format_str = "text";
|
||||||
|
const char *lang = "en", *i18n_dir_arg = NULL;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
const char *arg = argv[i];
|
||||||
|
int rc = 0;
|
||||||
|
if (strcmp(arg, "--seed") == 0) rc = require_arg(argc, argv, &i, arg, &seed);
|
||||||
|
else if (strcmp(arg, "--birth-date") == 0) rc = require_arg(argc, argv, &i, arg, &birth_date);
|
||||||
|
else if (strcmp(arg, "--birth-time") == 0) rc = require_arg(argc, argv, &i, arg, &birth_time);
|
||||||
|
else if (strcmp(arg, "--birth-utc-offset") == 0) rc = require_arg(argc, argv, &i, arg, &birth_utc_offset);
|
||||||
|
else if (strcmp(arg, "--birth-lat") == 0) rc = require_arg(argc, argv, &i, arg, &birth_lat);
|
||||||
|
else if (strcmp(arg, "--birth-lon") == 0) rc = require_arg(argc, argv, &i, arg, &birth_lon);
|
||||||
|
else if (strcmp(arg, "--date") == 0) rc = require_arg(argc, argv, &i, arg, &date_str);
|
||||||
|
else if (strcmp(arg, "--format") == 0) rc = require_arg(argc, argv, &i, arg, &format_str);
|
||||||
|
else if (strcmp(arg, "--lang") == 0) rc = require_arg(argc, argv, &i, arg, &lang);
|
||||||
|
else if (strcmp(arg, "--i18n-dir") == 0) rc = require_arg(argc, argv, &i, arg, &i18n_dir_arg);
|
||||||
|
else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Unknown argument: %s\n", arg);
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (rc != 0) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!seed || !birth_date || !birth_time || !birth_utc_offset || !birth_lat || !birth_lon) {
|
||||||
|
fprintf(stderr, "Missing required argument(s).\n\n");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BirthData birth = {0};
|
||||||
|
if (sscanf(birth_date, "%d-%d-%d", &birth.year, &birth.month, &birth.day) != 3) {
|
||||||
|
fprintf(stderr, "Invalid --birth-date, expected YYYY-MM-DD\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (sscanf(birth_time, "%d:%d", &birth.hour, &birth.minute) != 2) {
|
||||||
|
fprintf(stderr, "Invalid --birth-time, expected HH:MM\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
birth.utc_offset_hours = atof(birth_utc_offset);
|
||||||
|
birth.latitude = atof(birth_lat);
|
||||||
|
birth.longitude = atof(birth_lon);
|
||||||
|
|
||||||
|
time_t utc_moment;
|
||||||
|
if (date_str) {
|
||||||
|
struct tm tm_date = {0};
|
||||||
|
int second = 0;
|
||||||
|
if (sscanf(date_str, "%d-%d-%dT%d:%d:%d", &tm_date.tm_year, &tm_date.tm_mon,
|
||||||
|
&tm_date.tm_mday, &tm_date.tm_hour, &tm_date.tm_min, &second) < 5) {
|
||||||
|
fprintf(stderr, "Invalid --date, expected YYYY-MM-DDTHH:MM[:SS]\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
tm_date.tm_year -= 1900;
|
||||||
|
tm_date.tm_mon -= 1;
|
||||||
|
tm_date.tm_sec = second;
|
||||||
|
utc_moment = timegm(&tm_date);
|
||||||
|
} else {
|
||||||
|
utc_moment = time(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputFormat format;
|
||||||
|
if (strcmp(format_str, "text") == 0) format = FORMAT_TEXT;
|
||||||
|
else if (strcmp(format_str, "html") == 0) format = FORMAT_HTML;
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "Invalid --format, expected text or html\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char i18n_path[512];
|
||||||
|
if (i18n_dir_arg) {
|
||||||
|
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
|
||||||
|
} else {
|
||||||
|
default_i18n_path(argv[0], lang, i18n_path, sizeof i18n_path);
|
||||||
|
}
|
||||||
|
if (!i18n_load(i18n_path) && strcmp(lang, "en") != 0) {
|
||||||
|
fprintf(stderr, "warning: could not load translations for '%s' from %s - "
|
||||||
|
"falling back to built-in English\n", lang, i18n_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
DailyReading reading;
|
||||||
|
reading_generate(seed, &birth, utc_moment, &reading);
|
||||||
|
|
||||||
|
if (format == FORMAT_HTML) {
|
||||||
|
reading_print_html(&reading, stdout);
|
||||||
|
} else {
|
||||||
|
reading_print_text(&reading, stdout);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#include "reading.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
/* UI-label keys ("ui.*") are looked up here rather than in i18n.h/.c
|
||||||
|
* because they're specific to these two renderers, not part of the
|
||||||
|
* portable astro/tarot vocabulary. */
|
||||||
|
static const char *ui(const char *key, const char *fallback) {
|
||||||
|
return i18n_get(key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||||
|
time_t utc_moment, DailyReading *out) {
|
||||||
|
astro_compute_natal_chart(birth, &out->natal);
|
||||||
|
astro_compute_daily_transits(utc_moment, &out->natal, &out->transits);
|
||||||
|
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_position_deg(FILE *out, const PlanetPosition *p) {
|
||||||
|
fprintf(out, "%5.1f%s %s", p->degree_in_sign, "\xc2\xb0", astro_sign_name(p->sign));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reading_print_text(const DailyReading *r, FILE *out) {
|
||||||
|
fprintf(out, "===== %s =====\n", ui("ui.natal_chart", "Natal Chart"));
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
fprintf(out, "%-8s ", astro_body_name((Body)b));
|
||||||
|
print_position_deg(out, &r->natal.bodies[b]);
|
||||||
|
fprintf(out, "\n");
|
||||||
|
}
|
||||||
|
fprintf(out, "%s %5.1f%s %s\n", ui("ui.ascendant", "Ascendant"),
|
||||||
|
fmod(r->natal.ascendant_longitude, 30.0), "\xc2\xb0",
|
||||||
|
astro_sign_name(r->natal.houses[0]));
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
fprintf(out, "===== %s =====\n", ui("ui.todays_sky", "Today's Sky"));
|
||||||
|
fprintf(out, "%s %s\n", ui("ui.moon_phase", "Moon phase:"),
|
||||||
|
astro_moon_phase_name(r->transits.moon_phase));
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
fprintf(out, "%-8s ", astro_body_name((Body)b));
|
||||||
|
print_position_deg(out, &r->transits.bodies[b]);
|
||||||
|
fprintf(out, "\n");
|
||||||
|
}
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
fprintf(out, "===== %s =====\n", ui("ui.todays_aspects", "Today's Aspects to Natal Chart"));
|
||||||
|
if (r->transits.aspect_count == 0) {
|
||||||
|
fprintf(out, "%s\n", ui("ui.no_aspects", "(none within orb)"));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < r->transits.aspect_count; i++) {
|
||||||
|
const Aspect *a = &r->transits.aspects[i];
|
||||||
|
fprintf(out, "%s %s %s %s %s (%s %.1f%s)\n", ui("ui.transiting", "Transiting"),
|
||||||
|
astro_body_name(a->transiting_planet), astro_aspect_name(a->type),
|
||||||
|
ui("ui.natal", "natal"), astro_body_name(a->natal_planet),
|
||||||
|
ui("ui.orb", "orb"), a->orb, "\xc2\xb0");
|
||||||
|
}
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
const TarotDraw *draw = &r->spread.positions[i];
|
||||||
|
fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticCrossPosition)i),
|
||||||
|
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||||||
|
draw->reversed ? ui("ui.reversed", "(Reversed)") : "");
|
||||||
|
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
||||||
|
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_body_row_html(FILE *out, const char *label, const PlanetPosition *p) {
|
||||||
|
fprintf(out, "<tr><td>%s</td><td>%.1f° %s</td></tr>\n", label,
|
||||||
|
p->degree_in_sign, astro_sign_name(p->sign));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reading_print_html(const DailyReading *r, FILE *out) {
|
||||||
|
fprintf(out,
|
||||||
|
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
||||||
|
"<title>%s</title>\n"
|
||||||
|
"<style>\n"
|
||||||
|
"body { font-family: sans-serif; max-width: 900px; margin: 2em auto; }\n"
|
||||||
|
"table { border-collapse: collapse; margin-bottom: 1.5em; }\n"
|
||||||
|
"td { padding: 2px 10px 2px 0; }\n"
|
||||||
|
"h1, h2 { border-bottom: 1px solid #ccc; }\n"
|
||||||
|
".spread { display: flex; flex-wrap: wrap; gap: 1.5em; }\n"
|
||||||
|
".card { width: 160px; }\n"
|
||||||
|
".card img { width: 140px; display: block; }\n"
|
||||||
|
".card img.reversed { transform: rotate(180deg); }\n"
|
||||||
|
".card .position { font-weight: bold; }\n"
|
||||||
|
".card .name { font-style: italic; }\n"
|
||||||
|
"</style></head><body>\n",
|
||||||
|
ui("ui.page_title", "Deck in a Dash - Daily Reading"));
|
||||||
|
|
||||||
|
fprintf(out, "<h1>%s</h1>\n", ui("ui.daily_reading", "Daily Reading"));
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<table>\n", ui("ui.natal_chart", "Natal Chart"));
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
print_body_row_html(out, astro_body_name((Body)b), &r->natal.bodies[b]);
|
||||||
|
}
|
||||||
|
fprintf(out, "<tr><td>%s</td><td>%.1f° %s</td></tr>\n", ui("ui.ascendant", "Ascendant"),
|
||||||
|
fmod(r->natal.ascendant_longitude, 30.0), astro_sign_name(r->natal.houses[0]));
|
||||||
|
fprintf(out, "</table>\n");
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<p>%s %s</p>\n<table>\n", ui("ui.todays_sky", "Today's Sky"),
|
||||||
|
ui("ui.moon_phase", "Moon phase:"), astro_moon_phase_name(r->transits.moon_phase));
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
print_body_row_html(out, astro_body_name((Body)b), &r->transits.bodies[b]);
|
||||||
|
}
|
||||||
|
fprintf(out, "</table>\n");
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<table>\n", ui("ui.todays_aspects", "Today's Aspects to Natal Chart"));
|
||||||
|
for (int i = 0; i < r->transits.aspect_count; i++) {
|
||||||
|
const Aspect *a = &r->transits.aspects[i];
|
||||||
|
fprintf(out, "<tr><td>%s %s</td><td>%s</td><td>%s %s</td><td>%s %.1f°</td></tr>\n",
|
||||||
|
ui("ui.transiting", "Transiting"), astro_body_name(a->transiting_planet),
|
||||||
|
astro_aspect_name(a->type), ui("ui.natal", "natal"),
|
||||||
|
astro_body_name(a->natal_planet), ui("ui.orb", "orb"), a->orb);
|
||||||
|
}
|
||||||
|
fprintf(out, "</table>\n");
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
const TarotDraw *draw = &r->spread.positions[i];
|
||||||
|
fprintf(out,
|
||||||
|
"<div class=\"card\">\n"
|
||||||
|
" <div class=\"position\">%s</div>\n"
|
||||||
|
/* Expects card art at img/<file> next to the HTML file itself -
|
||||||
|
* `make images` copies res/img/ there alongside the binary in
|
||||||
|
* dist/. */
|
||||||
|
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
|
||||||
|
" <div class=\"name\">%s%s%s</div>\n"
|
||||||
|
" <div class=\"desc\">%s</div>\n"
|
||||||
|
" <div class=\"meaning\">%s</div>\n"
|
||||||
|
"</div>\n",
|
||||||
|
tarot_position_name((CelticCrossPosition)i),
|
||||||
|
draw->reversed ? "reversed" : "",
|
||||||
|
tarot_card_image_file(draw->card), tarot_card_name(draw->card),
|
||||||
|
tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||||||
|
draw->reversed ? ui("ui.reversed", "(Reversed)") : "",
|
||||||
|
tarot_position_description((CelticCrossPosition)i),
|
||||||
|
tarot_card_meaning(draw->card, draw->reversed));
|
||||||
|
}
|
||||||
|
fprintf(out, "</div>\n</body></html>\n");
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef DECK_READING_H
|
||||||
|
#define DECK_READING_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "astro.h"
|
||||||
|
#include "tarot.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
NatalChart natal;
|
||||||
|
DailyTransits transits;
|
||||||
|
CelticCrossSpread spread;
|
||||||
|
} DailyReading;
|
||||||
|
|
||||||
|
/* The real API: this is the only function the watchapp needs to call.
|
||||||
|
* It walks the returned struct directly to lay out its own screens. */
|
||||||
|
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||||
|
time_t utc_moment, DailyReading *out);
|
||||||
|
|
||||||
|
/* Desktop-only output formats for inspecting a reading before any watch
|
||||||
|
* UI exists. Not used by (and not needed by) the watchapp. */
|
||||||
|
void reading_print_text(const DailyReading *r, FILE *out);
|
||||||
|
void reading_print_html(const DailyReading *r, FILE *out);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include "rng.h"
|
||||||
|
|
||||||
|
uint64_t rng_seed_from_string(const char *seed) {
|
||||||
|
uint64_t hash = 0xcbf29ce484222325ULL; /* FNV offset basis */
|
||||||
|
for (const unsigned char *p = (const unsigned char *)seed; *p; p++) {
|
||||||
|
hash ^= (uint64_t)*p;
|
||||||
|
hash *= 0x100000001b3ULL; /* FNV prime */
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rng_init(RngState *rng, uint64_t seed) {
|
||||||
|
/* splitmix64 requires a nonzero-friendly state; any seed works, but
|
||||||
|
* avoid the degenerate all-zero stream for the empty string. */
|
||||||
|
rng->state = seed ? seed : 0x9e3779b97f4a7c15ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t rng_next_u64(RngState *rng) {
|
||||||
|
uint64_t z = (rng->state += 0x9e3779b97f4a7c15ULL);
|
||||||
|
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL;
|
||||||
|
z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL;
|
||||||
|
return z ^ (z >> 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t rng_next_bounded(RngState *rng, uint32_t bound) {
|
||||||
|
/* Rejection sampling against the largest multiple of `bound` that fits
|
||||||
|
* in 32 bits, so every outcome in [0, bound) is equally likely. `limit`
|
||||||
|
* must stay 64-bit: when `bound` evenly divides 2^32 (e.g. bound == 2,
|
||||||
|
* used for every reversed-card coin flip), the true limit is 2^32
|
||||||
|
* itself, which truncates to 0 in a uint32_t and turns the loop below
|
||||||
|
* into an infinite one. */
|
||||||
|
uint64_t limit = 0x100000000ULL - (0x100000000ULL % bound);
|
||||||
|
uint64_t value;
|
||||||
|
do {
|
||||||
|
value = rng_next_u64(rng) & 0xffffffffULL;
|
||||||
|
} while (value >= limit);
|
||||||
|
return (uint32_t)(value % bound);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef DECK_RNG_H
|
||||||
|
#define DECK_RNG_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t state;
|
||||||
|
} RngState;
|
||||||
|
|
||||||
|
/* FNV-1a 64-bit hash of a NUL-terminated string. */
|
||||||
|
uint64_t rng_seed_from_string(const char *seed);
|
||||||
|
|
||||||
|
void rng_init(RngState *rng, uint64_t seed);
|
||||||
|
|
||||||
|
/* splitmix64 stream. Pure integer arithmetic so the sequence is identical
|
||||||
|
* on every platform this engine runs on, including the watch later. */
|
||||||
|
uint64_t rng_next_u64(RngState *rng);
|
||||||
|
|
||||||
|
/* Uniform value in [0, bound). bound must be > 0. */
|
||||||
|
uint32_t rng_next_bounded(RngState *rng, uint32_t bound);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#include "tarot.h"
|
||||||
|
#include "rng.h"
|
||||||
|
|
||||||
|
void tarot_draw_celtic_cross(const char *seed, CelticCrossSpread *out) {
|
||||||
|
TarotCard deck[TAROT_DECK_SIZE];
|
||||||
|
for (int i = 0; i < TAROT_DECK_SIZE; i++) {
|
||||||
|
deck[i] = (TarotCard)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
RngState rng;
|
||||||
|
rng_init(&rng, rng_seed_from_string(seed));
|
||||||
|
|
||||||
|
/* Partial Fisher-Yates: only shuffle the prefix we actually need, one
|
||||||
|
* slot at a time, drawing the reversal bit for a card immediately
|
||||||
|
* after it's picked. Same distribution as a full shuffle, and the
|
||||||
|
* draw order this produces is exactly reproducible from the seed. */
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
int j = i + (int)rng_next_bounded(&rng, (uint32_t)(TAROT_DECK_SIZE - i));
|
||||||
|
TarotCard tmp = deck[i];
|
||||||
|
deck[i] = deck[j];
|
||||||
|
deck[j] = tmp;
|
||||||
|
|
||||||
|
out->positions[i].card = deck[i];
|
||||||
|
out->positions[i].reversed = rng_next_bounded(&rng, 2) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#ifndef DECK_TAROT_H
|
||||||
|
#define DECK_TAROT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define TAROT_DECK_SIZE 22
|
||||||
|
#define TAROT_SPREAD_SIZE 10
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CARD_FOOL = 0,
|
||||||
|
CARD_MAGICIAN,
|
||||||
|
CARD_HIGH_PRIESTESS,
|
||||||
|
CARD_EMPRESS,
|
||||||
|
CARD_EMPEROR,
|
||||||
|
CARD_HIEROPHANT,
|
||||||
|
CARD_LOVERS,
|
||||||
|
CARD_CHARIOT,
|
||||||
|
CARD_STRENGTH,
|
||||||
|
CARD_HERMIT,
|
||||||
|
CARD_WHEEL_OF_FORTUNE,
|
||||||
|
CARD_JUSTICE,
|
||||||
|
CARD_HANGED_MAN,
|
||||||
|
CARD_DEATH,
|
||||||
|
CARD_TEMPERANCE,
|
||||||
|
CARD_DEVIL,
|
||||||
|
CARD_TOWER,
|
||||||
|
CARD_STAR,
|
||||||
|
CARD_MOON,
|
||||||
|
CARD_SUN,
|
||||||
|
CARD_JUDGEMENT,
|
||||||
|
CARD_WORLD
|
||||||
|
} TarotCard;
|
||||||
|
|
||||||
|
/* Waite's original ten positions from "An Ancient Celtic Method of
|
||||||
|
* Divination" (The Pictorial Key to the Tarot, Part III §7), in his
|
||||||
|
* original drawing order. */
|
||||||
|
typedef enum {
|
||||||
|
POSITION_PRESENT = 0, /* "This covers him." */
|
||||||
|
POSITION_CHALLENGE, /* "This crosses him." */
|
||||||
|
POSITION_CROWN, /* "This crowns him." */
|
||||||
|
POSITION_FOUNDATION, /* "This is beneath him." */
|
||||||
|
POSITION_RECENT_PAST, /* "This is behind him." */
|
||||||
|
POSITION_NEAR_FUTURE, /* "This is before him." */
|
||||||
|
POSITION_ATTITUDE, /* "Himself." */
|
||||||
|
POSITION_ENVIRONMENT, /* "His house." */
|
||||||
|
POSITION_HOPES_AND_FEARS,
|
||||||
|
POSITION_OUTCOME
|
||||||
|
} CelticCrossPosition;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
TarotCard card;
|
||||||
|
bool reversed;
|
||||||
|
} TarotDraw;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/* Indexed by CelticCrossPosition. */
|
||||||
|
TarotDraw positions[TAROT_SPREAD_SIZE];
|
||||||
|
} CelticCrossSpread;
|
||||||
|
|
||||||
|
/* Deterministic: the same seed string always produces the same ten
|
||||||
|
* cards, in the same positions, with the same orientations. */
|
||||||
|
void tarot_draw_celtic_cross(const char *seed, CelticCrossSpread *out);
|
||||||
|
|
||||||
|
/* Card content, sourced from A. E. Waite's Pictorial Key to the Tarot
|
||||||
|
* (1911, public domain). Defined in tarot_data.c. */
|
||||||
|
const char *tarot_card_name(TarotCard card);
|
||||||
|
const char *tarot_card_image_file(TarotCard card); /* matches res/img/ */
|
||||||
|
const char *tarot_card_meaning(TarotCard card, bool reversed);
|
||||||
|
|
||||||
|
const char *tarot_position_name(CelticCrossPosition position);
|
||||||
|
const char *tarot_position_description(CelticCrossPosition position);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
#include "tarot.h"
|
||||||
|
#include "i18n.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Card meanings are condensed from A. E. Waite, The Pictorial Key to the
|
||||||
|
* Tarot (1911), Part III §3 "The Greater Arcana and their Divinatory
|
||||||
|
* Meanings" (public domain). Waite gives several alternative readings
|
||||||
|
* for some cards; these keep his own wording, trimmed to the core
|
||||||
|
* clauses. */
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
const char *image_file;
|
||||||
|
const char *upright;
|
||||||
|
const char *reversed;
|
||||||
|
} TarotCardInfo;
|
||||||
|
|
||||||
|
static const TarotCardInfo k_cards[TAROT_DECK_SIZE] = {
|
||||||
|
[CARD_FOOL] = {
|
||||||
|
"The Fool", "RWS_Tarot_00_Fool.jpeg",
|
||||||
|
"Folly, mania, extravagance, intoxication, delirium, frenzy.",
|
||||||
|
"Negligence, absence, carelessness, apathy, nullity, vanity."
|
||||||
|
},
|
||||||
|
[CARD_MAGICIAN] = {
|
||||||
|
"The Magician", "RWS_Tarot_01_Magician.jpeg",
|
||||||
|
"Skill, diplomacy, address, subtlety; self-confidence, will.",
|
||||||
|
"Physician, Magus, mental disease, disgrace, disquiet."
|
||||||
|
},
|
||||||
|
[CARD_HIGH_PRIESTESS] = {
|
||||||
|
"The High Priestess", "RWS_Tarot_02_High_Priestess.jpeg",
|
||||||
|
"Secrets, mystery, the future as yet unrevealed; silence, tenacity, wisdom, science.",
|
||||||
|
"Passion, moral or physical ardour, conceit, surface knowledge."
|
||||||
|
},
|
||||||
|
[CARD_EMPRESS] = {
|
||||||
|
"The Empress", "RWS_Tarot_03_Empress.jpeg",
|
||||||
|
"Fruitfulness, action, initiative, length of days; also difficulty, doubt, ignorance.",
|
||||||
|
"Light, truth, the unravelling of involved matters, public rejoicings; vacillation."
|
||||||
|
},
|
||||||
|
[CARD_EMPEROR] = {
|
||||||
|
"The Emperor", "RWS_Tarot_04_Emperor.jpeg",
|
||||||
|
"Stability, power, protection, realization; aid, reason, conviction, authority and will.",
|
||||||
|
"Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity."
|
||||||
|
},
|
||||||
|
[CARD_HIEROPHANT] = {
|
||||||
|
"The Hierophant", "RWS_Tarot_05_Hierophant.jpeg",
|
||||||
|
"Marriage, alliance, captivity, servitude; by another account, mercy, goodness, inspiration.",
|
||||||
|
"Society, good understanding, concord, over-kindness, weakness."
|
||||||
|
},
|
||||||
|
[CARD_LOVERS] = {
|
||||||
|
"The Lovers", "RWS_Tarot_06_Lovers.jpeg",
|
||||||
|
"Attraction, love, beauty, trials overcome.",
|
||||||
|
"Failure, foolish designs; marriage frustrated, contrarieties of all kinds."
|
||||||
|
},
|
||||||
|
[CARD_CHARIOT] = {
|
||||||
|
"The Chariot", "RWS_Tarot_07_Chariot.jpeg",
|
||||||
|
"Succour, providence; also war, triumph, presumption, vengeance, trouble.",
|
||||||
|
"Riot, quarrel, dispute, litigation, defeat."
|
||||||
|
},
|
||||||
|
[CARD_STRENGTH] = {
|
||||||
|
"Strength", "RWS_Tarot_08_Strength.jpeg",
|
||||||
|
"Power, energy, action, courage, magnanimity; complete success and honours.",
|
||||||
|
"Despotism, abuse of power, weakness, discord, sometimes even disgrace."
|
||||||
|
},
|
||||||
|
[CARD_HERMIT] = {
|
||||||
|
"The Hermit", "RWS_Tarot_09_Hermit.jpeg",
|
||||||
|
"Prudence, circumspection; also, and especially, treason, dissimulation, roguery, corruption.",
|
||||||
|
"Concealment, disguise, policy, fear, unreasoned caution."
|
||||||
|
},
|
||||||
|
[CARD_WHEEL_OF_FORTUNE] = {
|
||||||
|
"Wheel of Fortune", "RWS_Tarot_10_Wheel_of_Fortune.jpeg",
|
||||||
|
"Destiny, fortune, success, elevation, luck, felicity.",
|
||||||
|
"Increase, abundance, superfluity."
|
||||||
|
},
|
||||||
|
[CARD_JUSTICE] = {
|
||||||
|
"Justice", "RWS_Tarot_11_Justice.jpeg",
|
||||||
|
"Equity, rightness, probity, executive; triumph of the deserving side in law.",
|
||||||
|
"Law in all its departments, legal complications, bigotry, bias, excessive severity."
|
||||||
|
},
|
||||||
|
[CARD_HANGED_MAN] = {
|
||||||
|
"The Hanged Man", "RWS_Tarot_12_Hanged_Man.jpeg",
|
||||||
|
"Wisdom, circumspection, discernment, trials, sacrifice, intuition, divination, prophecy.",
|
||||||
|
"Selfishness, the crowd, body politic."
|
||||||
|
},
|
||||||
|
[CARD_DEATH] = {
|
||||||
|
"Death", "RWS_Tarot_13_Death.jpeg",
|
||||||
|
"End, mortality, destruction, corruption; loss of a benefactor, or many contrarieties.",
|
||||||
|
"Inertia, sleep, lethargy, petrifaction, somnambulism; hope destroyed."
|
||||||
|
},
|
||||||
|
[CARD_TEMPERANCE] = {
|
||||||
|
"Temperance", "RWS_Tarot_14_Temperance.jpeg",
|
||||||
|
"Economy, moderation, frugality, management, accommodation.",
|
||||||
|
"Things connected with churches, religions, sects, the priesthood; disunion, competing interests."
|
||||||
|
},
|
||||||
|
[CARD_DEVIL] = {
|
||||||
|
"The Devil", "RWS_Tarot_15_Devil.jpeg",
|
||||||
|
"Ravage, violence, vehemence, extraordinary efforts, force, fatality.",
|
||||||
|
"Evil fatality, weakness, pettiness, blindness."
|
||||||
|
},
|
||||||
|
[CARD_TOWER] = {
|
||||||
|
"The Tower", "RWS_Tarot_16_Tower.jpeg",
|
||||||
|
"Misery, distress, indigence, adversity, calamity, disgrace, deception, ruin; unforeseen catastrophe.",
|
||||||
|
"The same in a lesser degree; also oppression, imprisonment, tyranny."
|
||||||
|
},
|
||||||
|
[CARD_STAR] = {
|
||||||
|
"The Star", "RWS_Tarot_17_Star.jpeg",
|
||||||
|
"Loss, theft, privation, abandonment; another reading says hope and bright prospects.",
|
||||||
|
"Arrogance, haughtiness, impotence."
|
||||||
|
},
|
||||||
|
[CARD_MOON] = {
|
||||||
|
"The Moon", "RWS_Tarot_18_Moon.jpeg",
|
||||||
|
"Hidden enemies, danger, calumny, darkness, terror, deception, occult forces, error.",
|
||||||
|
"Instability, inconstancy, silence, lesser degrees of deception and error."
|
||||||
|
},
|
||||||
|
[CARD_SUN] = {
|
||||||
|
"The Sun", "RWS_Tarot_19_Sun.jpeg",
|
||||||
|
"Material happiness, fortunate marriage, contentment.",
|
||||||
|
"The same in a lesser sense."
|
||||||
|
},
|
||||||
|
[CARD_JUDGEMENT] = {
|
||||||
|
"Judgement", "RWS_Tarot_20_Judgement.jpeg",
|
||||||
|
"Change of position, renewal, outcome; another account specifies total loss through lawsuit.",
|
||||||
|
"Weakness, pusillanimity, simplicity; also deliberation, decision, sentence."
|
||||||
|
},
|
||||||
|
[CARD_WORLD] = {
|
||||||
|
"The World", "RWS_Tarot_21_World.jpeg",
|
||||||
|
"Assured success, recompense, voyage, route, emigration, flight, change of place.",
|
||||||
|
"Inertia, fixity, stagnation, permanence."
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Position names and one-line descriptions follow Waite's own account
|
||||||
|
* of "An Ancient Celtic Method of Divination" (Part III §7), in his
|
||||||
|
* original drawing order. */
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
const char *description;
|
||||||
|
} PositionInfo;
|
||||||
|
|
||||||
|
static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = {
|
||||||
|
[POSITION_PRESENT] = {
|
||||||
|
"The Present",
|
||||||
|
"This covers him: the general influence affecting the matter."
|
||||||
|
},
|
||||||
|
[POSITION_CHALLENGE] = {
|
||||||
|
"The Challenge",
|
||||||
|
"This crosses him: the nature of the obstacle in the matter."
|
||||||
|
},
|
||||||
|
[POSITION_CROWN] = {
|
||||||
|
"The Crown",
|
||||||
|
"This crowns him: the aim or ideal, the best that can be achieved."
|
||||||
|
},
|
||||||
|
[POSITION_FOUNDATION] = {
|
||||||
|
"The Foundation",
|
||||||
|
"This is beneath him: the basis of the matter, already actual."
|
||||||
|
},
|
||||||
|
[POSITION_RECENT_PAST] = {
|
||||||
|
"The Recent Past",
|
||||||
|
"This is behind him: the influence that is just passing away."
|
||||||
|
},
|
||||||
|
[POSITION_NEAR_FUTURE] = {
|
||||||
|
"The Near Future",
|
||||||
|
"This is before him: the influence now coming into action."
|
||||||
|
},
|
||||||
|
[POSITION_ATTITUDE] = {
|
||||||
|
"Himself",
|
||||||
|
"His position or attitude in the circumstances."
|
||||||
|
},
|
||||||
|
[POSITION_ENVIRONMENT] = {
|
||||||
|
"His House",
|
||||||
|
"His environment and the tendencies at work therein."
|
||||||
|
},
|
||||||
|
[POSITION_HOPES_AND_FEARS] = {
|
||||||
|
"Hopes and Fears",
|
||||||
|
"His hopes or fears in the matter."
|
||||||
|
},
|
||||||
|
[POSITION_OUTCOME] = {
|
||||||
|
"The Outcome",
|
||||||
|
"What will come: the final result of the matter."
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Slugs used to build i18n.c lookup keys ("card.<slug>.name", etc.) -
|
||||||
|
* translation files key off these, not the enum names, so renaming a C
|
||||||
|
* enumerator never silently breaks a .lang file. */
|
||||||
|
static const char *const k_card_slug[TAROT_DECK_SIZE] = {
|
||||||
|
[CARD_FOOL] = "fool",
|
||||||
|
[CARD_MAGICIAN] = "magician",
|
||||||
|
[CARD_HIGH_PRIESTESS] = "high_priestess",
|
||||||
|
[CARD_EMPRESS] = "empress",
|
||||||
|
[CARD_EMPEROR] = "emperor",
|
||||||
|
[CARD_HIEROPHANT] = "hierophant",
|
||||||
|
[CARD_LOVERS] = "lovers",
|
||||||
|
[CARD_CHARIOT] = "chariot",
|
||||||
|
[CARD_STRENGTH] = "strength",
|
||||||
|
[CARD_HERMIT] = "hermit",
|
||||||
|
[CARD_WHEEL_OF_FORTUNE] = "wheel_of_fortune",
|
||||||
|
[CARD_JUSTICE] = "justice",
|
||||||
|
[CARD_HANGED_MAN] = "hanged_man",
|
||||||
|
[CARD_DEATH] = "death",
|
||||||
|
[CARD_TEMPERANCE] = "temperance",
|
||||||
|
[CARD_DEVIL] = "devil",
|
||||||
|
[CARD_TOWER] = "tower",
|
||||||
|
[CARD_STAR] = "star",
|
||||||
|
[CARD_MOON] = "moon",
|
||||||
|
[CARD_SUN] = "sun",
|
||||||
|
[CARD_JUDGEMENT] = "judgement",
|
||||||
|
[CARD_WORLD] = "world",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
|
||||||
|
[POSITION_PRESENT] = "present",
|
||||||
|
[POSITION_CHALLENGE] = "challenge",
|
||||||
|
[POSITION_CROWN] = "crown",
|
||||||
|
[POSITION_FOUNDATION] = "foundation",
|
||||||
|
[POSITION_RECENT_PAST] = "recent_past",
|
||||||
|
[POSITION_NEAR_FUTURE] = "near_future",
|
||||||
|
[POSITION_ATTITUDE] = "attitude",
|
||||||
|
[POSITION_ENVIRONMENT] = "environment",
|
||||||
|
[POSITION_HOPES_AND_FEARS] = "hopes_and_fears",
|
||||||
|
[POSITION_OUTCOME] = "outcome",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *card_field(const char *slug, const char *field, const char *fallback) {
|
||||||
|
char key[64];
|
||||||
|
strcpy(key, "card.");
|
||||||
|
strcat(key, slug);
|
||||||
|
strcat(key, ".");
|
||||||
|
strcat(key, field);
|
||||||
|
return i18n_get(key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *position_field(const char *slug, const char *field, const char *fallback) {
|
||||||
|
char key[64];
|
||||||
|
strcpy(key, "position.");
|
||||||
|
strcat(key, slug);
|
||||||
|
strcat(key, ".");
|
||||||
|
strcat(key, field);
|
||||||
|
return i18n_get(key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tarot_card_name(TarotCard card) {
|
||||||
|
return card_field(k_card_slug[card], "name", k_cards[card].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tarot_card_image_file(TarotCard card) {
|
||||||
|
/* Not translated - it's a filename, not display text. */
|
||||||
|
return k_cards[card].image_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tarot_card_meaning(TarotCard card, bool reversed) {
|
||||||
|
return card_field(k_card_slug[card], reversed ? "reversed" : "upright",
|
||||||
|
reversed ? k_cards[card].reversed : k_cards[card].upright);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tarot_position_name(CelticCrossPosition position) {
|
||||||
|
return position_field(k_position_slug[position], "name", k_positions[position].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tarot_position_description(CelticCrossPosition position) {
|
||||||
|
return position_field(k_position_slug[position], "desc", k_positions[position].description);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "../src/reading.h"
|
||||||
|
#include "../src/i18n.h"
|
||||||
|
|
||||||
|
static void test_tarot_determinism(void) {
|
||||||
|
CelticCrossSpread a, b, c;
|
||||||
|
tarot_draw_celtic_cross("2026-07-03", &a);
|
||||||
|
tarot_draw_celtic_cross("2026-07-03", &b);
|
||||||
|
tarot_draw_celtic_cross("2026-07-04", &c);
|
||||||
|
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
assert(a.positions[i].card == b.positions[i].card);
|
||||||
|
assert(a.positions[i].reversed == b.positions[i].reversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int differs = 0;
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
if (a.positions[i].card != c.positions[i].card ||
|
||||||
|
a.positions[i].reversed != c.positions[i].reversed) {
|
||||||
|
differs = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(differs && "different seeds should (almost always) differ");
|
||||||
|
|
||||||
|
/* No repeated cards within a single spread. */
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
for (int j = i + 1; j < TAROT_SPREAD_SIZE; j++) {
|
||||||
|
assert(a.positions[i].card != a.positions[j].card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("PASS test_tarot_determinism\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_natal_sun_sign(void) {
|
||||||
|
/* 2000-06-15 noon UTC: the Sun is well inside Gemini (60-90 degrees),
|
||||||
|
* away from any sign boundary, so this is a stable regression check
|
||||||
|
* against the ephemeris math. Birth location doesn't affect the Sun's
|
||||||
|
* geocentric ecliptic longitude, only the Ascendant. */
|
||||||
|
BirthData birth = {
|
||||||
|
.year = 2000, .month = 6, .day = 15,
|
||||||
|
.hour = 12, .minute = 0,
|
||||||
|
.utc_offset_hours = 0.0,
|
||||||
|
.latitude = 52.5, .longitude = 13.4,
|
||||||
|
};
|
||||||
|
|
||||||
|
NatalChart chart;
|
||||||
|
astro_compute_natal_chart(&birth, &chart);
|
||||||
|
|
||||||
|
assert(chart.bodies[PLANET_SUN].sign == SIGN_GEMINI);
|
||||||
|
printf("PASS test_natal_sun_sign\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reading_generate_smoke(void) {
|
||||||
|
BirthData birth = {
|
||||||
|
.year = 1990, .month = 5, .day = 14,
|
||||||
|
.hour = 14, .minute = 32,
|
||||||
|
.utc_offset_hours = 2.0,
|
||||||
|
.latitude = 52.52, .longitude = 13.405,
|
||||||
|
};
|
||||||
|
|
||||||
|
DailyReading r1, r2;
|
||||||
|
reading_generate("smoke-test-seed", &birth, 1751500000, &r1);
|
||||||
|
reading_generate("smoke-test-seed", &birth, 1751500000, &r2);
|
||||||
|
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
assert(r1.spread.positions[i].card == r2.spread.positions[i].card);
|
||||||
|
assert(r1.spread.positions[i].reversed == r2.spread.positions[i].reversed);
|
||||||
|
}
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
assert(r1.natal.bodies[b].ecliptic_longitude == r2.natal.bodies[b].ecliptic_longitude);
|
||||||
|
assert(r1.transits.bodies[b].ecliptic_longitude == r2.transits.bodies[b].ecliptic_longitude);
|
||||||
|
}
|
||||||
|
assert(r1.transits.aspect_count == r2.transits.aspect_count);
|
||||||
|
|
||||||
|
printf("PASS test_reading_generate_smoke\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run with cwd == engine/ (as `make test` does), so the shipped
|
||||||
|
* engine/i18n/ .lang files are reachable as i18n/<code>.lang. */
|
||||||
|
static void test_i18n_fallback_and_translation(void) {
|
||||||
|
/* No catalog loaded yet: every lookup returns the caller's fallback. */
|
||||||
|
assert(strcmp(i18n_get("does.not.exist", "fallback text"), "fallback text") == 0);
|
||||||
|
|
||||||
|
bool loaded = i18n_load("i18n/de.lang");
|
||||||
|
assert(loaded && "expected engine/i18n/de.lang to exist and load");
|
||||||
|
assert(strcmp(i18n_get("body.sun", "Sun"), "Sonne") == 0);
|
||||||
|
assert(strcmp(i18n_get("does.not.exist", "fallback text"), "fallback text") == 0);
|
||||||
|
|
||||||
|
/* astro_ and tarot_ accessors route through the same loaded catalog. */
|
||||||
|
assert(strcmp(astro_body_name(PLANET_MOON), "Mond") == 0);
|
||||||
|
assert(strcmp(astro_sign_name(SIGN_LEO), "Löwe") == 0);
|
||||||
|
assert(strcmp(astro_moon_phase_name(MOON_FULL), "Vollmond") == 0);
|
||||||
|
assert(strcmp(astro_aspect_name(ASPECT_TRINE), "Trigon") == 0);
|
||||||
|
assert(strcmp(tarot_card_name(CARD_WORLD), "Die Welt") == 0);
|
||||||
|
assert(strcmp(tarot_position_name(POSITION_OUTCOME), "Das Ergebnis") == 0);
|
||||||
|
|
||||||
|
/* Loading a file that doesn't exist fails and leaves the previously
|
||||||
|
* loaded catalog in place, rather than silently clearing it. */
|
||||||
|
assert(!i18n_load("i18n/does-not-exist.lang"));
|
||||||
|
assert(strcmp(i18n_get("body.sun", "Sun"), "Sonne") == 0);
|
||||||
|
|
||||||
|
/* Loading a different language replaces the catalog wholesale. */
|
||||||
|
loaded = i18n_load("i18n/en.lang");
|
||||||
|
assert(loaded);
|
||||||
|
assert(strcmp(astro_body_name(PLANET_MOON), "Moon") == 0);
|
||||||
|
|
||||||
|
printf("PASS test_i18n_fallback_and_translation\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_tarot_determinism();
|
||||||
|
test_natal_sun_sign();
|
||||||
|
test_reading_generate_smoke();
|
||||||
|
test_i18n_fallback_and_translation();
|
||||||
|
printf("All smoke tests passed.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019-2025 Don Cross <cosinekitty@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Vendored from https://github.com/cosinekitty/astronomy
|
||||||
|
|
||||||
|
- Files: `source/c/astronomy.h`, `source/c/astronomy.c`
|
||||||
|
- Pinned commit: `865d3da7d8112bbc7911238052c6af4aaf877181` (master, 2025-01-27)
|
||||||
|
- License: MIT (see `LICENSE` in this directory)
|
||||||
|
- Unmodified from upstream.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
To calculate daily celestial events for astrology software, you must integrate an ephemeris engine that computes precise planetary coordinates and then apply astrological algorithms to interpret those positions as transits, aspects, and house placements. [1, 2]
|
||||||
|
## 1. Choose a Calculation Engine
|
||||||
|
The industry standard for professional astrology software is the [Swiss Ephemeris](https://roxyapi.com/blogs/swiss-ephemeris-explained-developers) (SE), a C library from Astrodienst AG. [2, 3]
|
||||||
|
|
||||||
|
* Accuracy: It provides sub-milliarcsecond precision based on NASA JPL data.
|
||||||
|
* Availability: It is available as a C library, but popular wrappers exist for Python ([Pyswisseph](https://vedika.io/blog/astrology-software-development-guide)), Java, and Node.js.
|
||||||
|
* Licensing: It uses an AGPL license, meaning you must open-source your code unless you purchase a commercial license. [2, 3, 4, 5]
|
||||||
|
|
||||||
|
## 2. Implement the Daily Logic Loop
|
||||||
|
Your software should follow a structured computational sequence to describe daily events:
|
||||||
|
|
||||||
|
1. Coordinate Conversion: Collect user location/time and convert it to Universal Time (UTC). [1, 6, 7]
|
||||||
|
2. Ephemeris Query: Call the engine to get the longitude and latitude of the Sun, Moon, and planets. [3, 8]
|
||||||
|
3. House Calculation: Use your chosen system (e.g., Placidus, Koch, or Whole Sign) to determine house cusps based on the time and location. [9, 10]
|
||||||
|
4. Aspect Detection: Calculate the angular distances between planets (e.g., a 120° angle is a Trine) and check if they fall within a specific "orb" of influence (typically 1° to 10°). [1, 11, 12, 13]
|
||||||
|
5. Transit Analysis: Compare current planetary positions to a user's Natal Chart to generate personalized daily insights. [9, 14]
|
||||||
|
|
||||||
|
## 3. Key Development Tools
|
||||||
|
|
||||||
|
| Category [1, 2, 11, 15, 16] | Recommended Tools |
|
||||||
|
|---|---|
|
||||||
|
| Libraries | Kerykeion[](https://kerykeion.net/) (Python), Stellium[](https://github.com/katelouie/stellium) (Python), Astronomy Engine[](https://github.com/cosinekitty/astronomy) (JS/C++/Python). |
|
||||||
|
| APIs | Vedika API[](https://vedika.io/blog/how-to-build-astrology-api) or Roxy Ephemeris for managed calculations without local engine hosting. |
|
||||||
|
| Automation | Use cron jobs (e.g., 0 0 * * *) to refresh planetary positions and daily forecasts every 24 hours. |
|
||||||
|
|
||||||
|
## 4. Important Calculations for Daily Content
|
||||||
|
|
||||||
|
* Ingresses: Tracking when a planet enters a new zodiac sign.
|
||||||
|
* Stations & Retrogrades: Identifying when a planet appears to slow down (stationary) or move backward.
|
||||||
|
* Planetary Hours: Dividing day and night into 12 "unequal hours" each, assigned to traditional planets.
|
||||||
|
* Lunar Cycles: Calculating the Moon's phase (New, Full, etc.) and its current sign (changes every ~2.5 days). [9, 17, 18, 19, 20]
|
||||||
|
|
||||||
|
If you are building the backend now, would you like to see a Python code snippet for a basic aspect calculation, or do you need a breakdown of different house systems?
|
||||||
|
|
||||||
|
[1] [https://dev.to](https://dev.to/kamal_deeppareek_f5bb5d8/how-to-integrate-planetary-data-into-your-astrology-app-2cgo)
|
||||||
|
[2] [https://roxyapi.com](https://roxyapi.com/blogs/swiss-ephemeris-explained-developers)
|
||||||
|
[3] [https://dev.to](https://dev.to/kamal_deeppareek_f5bb5d8/how-to-integrate-planetary-data-into-your-astrology-app-2cgo)
|
||||||
|
[4] [https://vedika.io](https://vedika.io/blog/astrology-software-development-guide)
|
||||||
|
[5] [https://en.wikipedia.org](https://en.wikipedia.org/wiki/Astrology_software)
|
||||||
|
[6] [https://dev.to](https://dev.to/kamal_deeppareek_f5bb5d8/how-to-integrate-planetary-data-into-your-astrology-app-2cgo)
|
||||||
|
[7] [https://kerykeion.net](https://kerykeion.net/astrologer-api/guides/astrology-software-development)
|
||||||
|
[8] [https://www.quora.com](https://www.quora.com/Does-astrology-software-enter-the-full-ephemeris-in-the-software-to-calculate-planetary-positions-and-the-ascendant-or-are-there-some-formulae-to-calculate-the-same)
|
||||||
|
[9] [https://www.youtube.com](https://www.youtube.com/watch?v=TnoG0DR9SMY)
|
||||||
|
[10] [https://www.reddit.com](https://www.reddit.com/r/Advancedastrology/comments/1rss61c/is_there_an_astronomical_algorithms_for_astrology/)
|
||||||
|
[11] https://kerykeion.net
|
||||||
|
[12] [https://algocademy.com](https://algocademy.com/blog/the-unexpected-connection-between-coding-and-astrology-unlocking-the-secrets-of-the-digital-cosmos/)
|
||||||
|
[13] [https://support.astrograph.com](https://support.astrograph.com/support/solutions/articles/66000476614-desktop-manual-timepassages)
|
||||||
|
[14] [https://www.molecularcloud.org](https://www.molecularcloud.org/p/how-do-you-create-a-real-time-astrology-prediction-app)
|
||||||
|
[15] [https://github.com](https://github.com/katelouie/stellium)
|
||||||
|
[16] [https://vedika.io](https://vedika.io/blog/how-to-build-astrology-api)
|
||||||
|
[17] [https://www.youtube.com](https://www.youtube.com/watch?v=yfO_v55XW_0)
|
||||||
|
[18] [https://kerykeion.net](https://kerykeion.net/content/learn-astrology/guide-timing-techniques)
|
||||||
|
[19] [https://play.google.com](https://play.google.com/store/apps/details?id=info.thereisonlywe.planetarytimes)
|
||||||
|
[20] [https://www.llewellyn.com](https://www.llewellyn.com/journal/article/534)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
The term "Celtic Tarot" most often refers to the Celtic Cross spread, which is the most widely used 10-card layout in divination. It also describes a category of tarot decks that replace traditional symbols (like the Hierophant) with Celtic archetypes (like the Druid). [1, 2, 3, 4, 5]
|
||||||
|
## The Celtic Cross Spread
|
||||||
|
The layout is divided into two parts: the Cross (six cards on the left) representing current energies, and the Staff (four cards on the right) representing external influences and the final path. [6, 7, 8]
|
||||||
|
|
||||||
|
[The Celtic Cross Tarot Spread - A Classic 10 Card Tarot ...](https://labyrinthos.co/blogs/learn-tarot-with-labyrinthos-academy/the-celtic-cross-tarot-spread-exploring-the-classic-10-card-tarot-spread)
|
||||||
|
[Breaking Down the Celtic Cross - Lesson 1: Laying it out](https://www.thetarotlady.com/breaking-celtic-cross-lesson-1-laying/)
|
||||||
|
[How to Read a Celtic Cross Tarot Spread and Gain Deep ...](https://astrostyle.com/tarot/how-to-read-the-celtic-cross-tarot-spread/)
|
||||||
|
[How to Read the Celtic Cross Tarot Spread | The Pagan Grimoire](https://www.pagangrimoire.com/how-to-read-the-celtic-cross-tarot-spread/)
|
||||||
|
[The Celtic Cross Tarot Spread (My Favorite) - Tarot Spreads ...](https://forum.thetarot.guru/t/the-celtic-cross-tarot-spread-my-favorite/294)
|
||||||
|
[How to Read The Celtic Cross Spread – Truly Teach Me Tarot](https://teachmetarot.com/celtic-cross-spread/how-to-read-the-celtic-cross-spread/)
|
||||||
|
[Celtic Cross Tarot Spread Tutorial (explaining the positions ...](https://www.youtube.com/watch?v=ma30RYGonws)
|
||||||
|
[Celtic Cross Tarot: Meaning, Spread Positions, and How to ...](https://www.theirishjewelrycompany.com/blog/post/celtic-cross-tarot-meaning-spread-positions)
|
||||||
|
|
||||||
|
| Position [3, 6, 7, 9, 10, 11, 12] | Meaning | Key Focus |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. The Present | Heart of the matter | Current situation or state of mind |
|
||||||
|
| 2. The Challenge | What "crosses" you | Immediate obstacles or supporting forces |
|
||||||
|
| 3. The Foundation | Distant past | Root causes or subconscious influences |
|
||||||
|
| 4. Recent Past | Passing events | Influences that led to the present |
|
||||||
|
| 5. Higher Power | Conscious goals | Best possible outcome or aspirations |
|
||||||
|
| 6. Near Future | Next steps | Immediate events approaching soon |
|
||||||
|
| 7. The Self | Your approach | Your current internal perspective |
|
||||||
|
| 8. External | Your environment | Influences from others and your surroundings |
|
||||||
|
| 9. Hopes/Fears | Inner state | Deepest anxieties or desired resolutions |
|
||||||
|
| 10. Final Outcome | Likely result | Long-term result based on current energy |
|
||||||
|
|
||||||
|
## Popular Celtic-Themed Decks
|
||||||
|
If you are looking for a deck inspired by Celtic mythology rather than just the spread, several highly-regarded options exist:
|
||||||
|
|
||||||
|
* [DruidCraft Tarot](https://druidry.org/druid-way/other-paths/wicca-druidcraft/druidcraft-tarot): Combines Wiccan and Druidic traditions; noted for its rich, earthy artwork.
|
||||||
|
* Celtic Spirit Tarot: An expanded 92-card deck that incorporates Celtic shamanism and an extra "Spirit" suit.
|
||||||
|
* Universal Celtic Tarot: A [Lo Scarabeo](https://www.loscarabeo.com/en) deck that weaves traditional tarot with myths of gods like Lugh and Brigid.
|
||||||
|
* Celtic Elemental Tarot: Renames traditional suits to Earth, Air, Water, and Fire, and features figures like King Arthur. [13, 14, 15, 16, 17]
|
||||||
|
|
||||||
|
## Origins and History
|
||||||
|
Contrary to popular belief, the ancient Celts did not use tarot; they likely used Ogham or runes for divination. The "Celtic Cross" spread was popularized (and likely invented) by [A.E. Waite](https://en.wikipedia.org/wiki/A._E._Waite) in 1911 in his book [The Pictorial Key to the Tarot](https://www.amazon.de/Pictorial-Key-Tarot-Divination-Illustrated/dp/1614273030). He marketed it as an "ancient" method to give it credibility during the occult revival of that era. [18, 19, 20, 21, 22]
|
||||||
|
Would you like a step-by-step guide on how to perform the Celtic Cross spread yourself, or are you looking for recommendations for a specific Celtic-themed deck?
|
||||||
|
|
||||||
|
[1] [https://www.youtube.com](https://www.youtube.com/shorts/zK6d7glfWN8)
|
||||||
|
[2] [https://www.learntarot.com](https://www.learntarot.com/ccross.htm)
|
||||||
|
[3] [https://www.theirishjewelrycompany.com](https://www.theirishjewelrycompany.com/blog/post/celtic-cross-tarot-meaning-spread-positions)
|
||||||
|
[4] [https://jackofwandstarot.wordpress.com](https://jackofwandstarot.wordpress.com/2018/03/07/a-review-of-the-celtic-tarot/)
|
||||||
|
[5] [https://www.youtube.com](https://www.youtube.com/watch?v=_-nvrQkfCF4)
|
||||||
|
[6] [https://celticstudio.shop](https://celticstudio.shop/blogs/article/celtic-cross-in-tarot-meaning-and-spread)
|
||||||
|
[7] [https://elvitarot.com](https://elvitarot.com/blog/celtic-cross-tarot-spread/)
|
||||||
|
[8] [https://science.howstuffworks.com](https://science.howstuffworks.com/science-vs-myth/extrasensory-perceptions/celtic-cross-spread.htm)
|
||||||
|
[9] [https://www.thetarotlady.com](https://www.thetarotlady.com/breaking-celtic-cross-lesson-1-laying/)
|
||||||
|
[10] [https://www.youtube.com](https://www.youtube.com/watch?v=Za-0683VWYg&t=381)
|
||||||
|
[11] [https://www.thetarotlady.com](https://www.thetarotlady.com/breaking-celtic-cross-lesson-1-laying/)
|
||||||
|
[12] [https://forum.thetarot.guru](https://forum.thetarot.guru/t/the-celtic-cross-tarot-spread-my-favorite/294)
|
||||||
|
[13] [https://www.amazon.de](https://www.amazon.de/-/en/Celtic-Tarot-Lo-Scarabeo/dp/0738700134)
|
||||||
|
[14] [https://www.reddit.com](https://www.reddit.com/r/tarot/comments/184p9as/can_anyone_recommend_tarot_or_oracle_decks_with_a/)
|
||||||
|
[15] [https://www.youtube.com](https://www.youtube.com/watch?v=OVO05l9i8AA&t=161)
|
||||||
|
[16] [https://www.youtube.com](https://www.youtube.com/watch?v=9GJQFtDXpQc&t=15)
|
||||||
|
[17] [https://www.youtube.com](https://www.youtube.com/watch?v=TsZYRyQabdk&t=2)
|
||||||
|
[18] [https://www.quora.com](https://www.quora.com/Did-Celtics-Vikings-and-Normans-do-tarot-cards)
|
||||||
|
[19] [https://www.occult.live](https://www.occult.live/index.php?title=Celtic_Cross&mobileaction=toggle_view_desktop)
|
||||||
|
[20] [https://tarotandkitchenwitchery.com](https://tarotandkitchenwitchery.com/celtic-cross-tarot-spread/)
|
||||||
|
[21] [https://www.amazon.com.au](https://www.amazon.com.au/Ogham-Divining-25-Card-192-Page-Guidebook/dp/1578638909)
|
||||||
|
[22] [https://www.solacely.co](https://www.solacely.co/blogs/astrology-and-crystals/celtic-rune-symbols)
|
||||||
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 984 KiB |
|
After Width: | Height: | Size: 914 KiB |
|
After Width: | Height: | Size: 1003 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 944 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1017 KiB |
|
After Width: | Height: | Size: 1023 KiB |
|
After Width: | Height: | Size: 576 KiB |
|
After Width: | Height: | Size: 1008 KiB |
|
After Width: | Height: | Size: 972 KiB |
|
After Width: | Height: | Size: 868 KiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 789 KiB |
|
After Width: | Height: | Size: 695 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 986 KiB |
|
After Width: | Height: | Size: 1024 KiB |
|
After Width: | Height: | Size: 997 KiB |
|
After Width: | Height: | Size: 1006 KiB |
@@ -0,0 +1 @@
|
|||||||
|
https://www.learntarot.com/cards.htm
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |