The out-of-tree Bunnyland plugin package bunnyland_fortunesim (plugin id
bunnyland.fortunesim).
Tests run against a sibling bunnyland-server checkout without installing anything —
tests/conftest.py puts both this package's src/ and ../bunnyland-server/src on
sys.path. From this server/ directory:
# uses the sibling bunnyland-server's virtualenv/deps
uv run --project ../../bunnyland-server -m pytest
# or, if bunnyland + relics are already importable:
python -m pytestLint:
uv run --project ../../bunnyland-server ruff check src testsbunnyland serve --module bunnyland_fortunesimdefault_enabled=True, so no --plugin flag is required once the module is imported.
- Components —
LuckComponent(an open stat other packs can read),CharmComponent,OmenComponent,FortuneToolComponent,DivinerComponent,JinxComponent, and the typedReadingedge. - Luck —
LuckConsequencerecomputes each character's materialised luckvalueevery tick frombaseluck, the sum of held charms, and any activeward-luckritual bonus (which expires on its own epoch), emittingLuckChangedEventon band crossings.luck_fragmentsrender the first-person luck-band line ("Fortune favors you today."). - Charms & talismans — a held
CharmComponentshifts its holder's luck; a negative one is a cursed trinket.charm_fragmentsrender the first-person "you carry a lucky …" line, andspawn_charm/spawn_talisman/spawn_cursed_trinketmake the items. - Omens —
OmenConsequencecasts a room foreboding (a cursed charm loose on the floor) or auspicious (a lucky one), picking the omen line deterministically and clearing it when the charms leave. Sticky worldgen omens are left untouched.omen_fragmentsrender the room's omen;OmenSightedEvent/OmenClearedEventfire on change. - Fortune-telling — the
read-fortuneverb over a held tarot/tea-leaves tool assembles a deterministic reading (biased upward by the seeker's luck and weaving in the room omen) and emits a privateFortuneReadEvent.spawn_fortune_toolmakes the tool. - Superstitions — the
ward-luckverb (knock-on-wood, toss-salt, cross-fingers) grants a temporary luck boost, granting aLuckComponenton the fly if the character lacks one, and emitsWardLuckEvent. - Tarot readings (v2 headline) — the
read-tarotverb: a diviner reads a reachable client from a held deck. The drawn card uses controlled randomness — a per-divinerDivinerComponentdraw counter advanced each reading and hashed with theepochover the sortedTAROT_DECK, so a fixed(counter, epoch)always draws the same upright/reversed card while successive draws move on. A toned card leaves a mood, rapport grows through the coreSocialBondedge, the draw is recorded as a typedReadingedge, and an imminent storyteller incident is foreshadowed. EmitsTarotReadEvent. - Narrative jinxes (v2 headline) — reworked curses, not a stat debuff.
lay-jinx(held cursed token) starts an escalatingJinxComponentrun;JinxConsequenceadvances a mishap on the storyteller's cadence (JinxMishapEvent), escalating through theMISHAP_STAGESuntil it runs its course, and feedsThreatPointsComponentpressure while active.break-jinx(held lucky charm) lifts it. EmitsJinxLaidEvent/JinxMishapEvent/JinxLiftedEvent. - Mood reuse — good/bad fortune, tarot tones, and jinx mishaps colour a character's mood by
reusing the core affect system: a decaying
ThoughtComponentcarrying anAffectDelta, which the stockAffectAggregationfolds into the character'sAffectComponent. - Worldgen —
FortuneWorldgenHooktags generated charm/trinket objects withCharmComponentand casts ominous/auspicious generated rooms with a stickyOmenComponent.
Every luck-biased outcome and fortune reading comes from a hashlib digest of stable ids plus
the world epoch, reduced over sorted tables (bands.digest_unit, bands.biased_index,
bands.luck_multiplier). No random, no wall-clock time — results are stable across machines
and across PYTHONHASHSEED. Tarot divination is the one sanctioned exception, and it stays within
the rule: it hashes a per-draw counter (advanced each reading) with the epoch, so it is
unpredictable to the player yet reproducible in tests and hash-seed-independent.