Add core engine: tarot + astrology reading generator with CLI and i18n
Plain-C, dependency-free engine that produces a personalized daily Celtic Cross tarot spread and natal-chart/transit astrology reading, plus a standalone CLI (dist/deck-engine) and run.sh wrapper. Includes English/German output via a simple key=value translation format, and docs/diagrams describing the architecture and I/O.
This commit is contained in:
@@ -0,0 +1,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;
|
||||
}
|
||||
Vendored
+21
@@ -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.
|
||||
+6
@@ -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.
|
||||
+13061
File diff suppressed because it is too large
Load Diff
+1431
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user