Skip to content

Commit a58456c

Browse files
thalismindclaude
andcommitted
Add bunnyland-fortunesim plugin: luck, charms, omens, fortune-telling, superstitions
Out-of-tree Bunnyland plugin (plugin id `bunnyland.fortunesim`, package `bunnyland_fortunesim`) bundling five cooperating mechanics, each in its own module with its own test file: - Luck stat: an open `LuckComponent` whose materialised `value` is maintained each tick by `LuckConsequence` (base + held charms + active ritual bonus), emitting `LuckChangedEvent` on band crossings. - Charms & talismans: carried `CharmComponent` items that raise (or, when cursed, lower) their holder's luck. - Omens: `OmenConsequence` casts rooms foreboding/auspicious from loose charms, leaving sticky worldgen omens alone; `OmenSightedEvent`/`OmenClearedEvent`. - Fortune-telling: the `read-fortune` verb over a held tarot/tea-leaves tool, assembling a deterministic reading biased by luck and the room omen. - Superstitions: the `ward-luck` verb (knock-on-wood, toss-salt, cross-fingers) buys a temporary luck boost. Good/bad fortune colours mood by reusing the core affect system. Every luck-biased outcome and fortune reading is derived from a hashlib digest of stable ids plus the world epoch, reduced over sorted tables — no random, no wall-clock time, deterministic across PYTHONHASHSEED. A worldgen hook tags generated charms/trinkets and casts ominous/auspicious rooms. Mirrors the bunnyland-spectersim repo layout and APIs. 73 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 parents  commit a58456c

28 files changed

Lines changed: 3399 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
server:
12+
name: Server Plugin (lint + tests)
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout fortunesim plugin
16+
uses: actions/checkout@v4
17+
with:
18+
path: bunnyland-fortunesim
19+
20+
# The plugin runs against a sibling bunnyland-server checkout: tests/conftest.py
21+
# puts ../bunnyland-server/src on sys.path, and its virtualenv provides bunnyland +
22+
# relics + ruff. This mirrors local development instead of building a container.
23+
- name: Checkout bunnyland-server
24+
uses: actions/checkout@v4
25+
with:
26+
repository: thalismind/bunnyland-server
27+
ref: main
28+
path: bunnyland-server
29+
token: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Install uv
32+
uses: astral-sh/setup-uv@v5
33+
34+
- name: Lint
35+
working-directory: bunnyland-fortunesim/server
36+
run: uv run --project ../../bunnyland-server ruff check src tests
37+
38+
- name: Test
39+
working-directory: bunnyland-fortunesim/server
40+
run: uv run --project ../../bunnyland-server -m pytest

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
dist/
2+
build/
3+
*.egg-info/
4+
artifacts/
5+
*.log
6+
.pytest_cache/
7+
.ruff_cache/
8+
__pycache__/
9+
*.py[cod]
10+
.venv/

CODE_OF_CONDUCT.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Code of Conduct
2+
3+
This plugin follows the Bunnyland project code of conduct:
4+
5+
https://github.com/thalismind/bunnyland-server/blob/main/CODE_OF_CONDUCT.md

CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Contributing
2+
3+
This plugin follows the Bunnyland project contribution guidelines:
4+
5+
https://github.com/thalismind/bunnyland-server/blob/main/CONTRIBUTING.md

LICENSE

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Bunnyland Fortunesim
2+
3+
Out-of-tree [Bunnyland](https://github.com/thalismind/bunnyland-server) plugin about **luck,
4+
charms, and omens** — an expansion-pack-sized themed bundle that gently nudges outcomes and
5+
rewards ritual behavior. It pairs wickedly with a haunting pack (bad omens near the dead) and a
6+
fishing pack (a lucky charm for the big catch).
7+
8+
> **Luck is a stat. Superstition is a strategy.**
9+
10+
The bundle ships five cooperating mechanics:
11+
12+
- **Luck stat** — an open `LuckComponent` on characters. Its materialised `value` (base +
13+
charms + active rituals) is maintained every tick by `LuckConsequence`, so any pack that opts
14+
in can read one field to bias its own probabilistic outcomes. Luck shifts the *weighting*; it
15+
never rolls dice.
16+
- **Charms & talismans** — carried `CharmComponent` items (a rabbit's foot, a four-leaf clover)
17+
that raise luck while held; cursed trinkets lower it.
18+
- **Omens**`OmenConsequence` reads the world and casts a room foreboding or auspicious,
19+
surfacing a deterministic omen line ("A crow watches you from the rafters.") into prompts.
20+
- **Fortune-telling** — a `read-fortune` verb over a held tarot / tea-leaves tool that assembles
21+
a deterministic reading from the seeker, their luck, and the current room omen.
22+
- **Superstitions** — a `ward-luck` verb (knock on wood, toss salt, cross fingers) that buys a
23+
temporary luck boost — fun busy-work with a real payoff.
24+
25+
**Determinism:** every luck-biased outcome and fortune reading is derived from a `hashlib`
26+
digest of stable ids plus the world `epoch`, reduced over sorted tables. There is no `random`
27+
and no wall-clock time anywhere in the package, so the same world state always yields the same
28+
result regardless of `PYTHONHASHSEED`.
29+
30+
This repo intentionally keeps all of the fortune work outside the main `bunnyland-server` repo.
31+
32+
## Layout
33+
34+
- `server/` — Python Bunnyland plugin package with the luck/charm/omen components, the luck and
35+
omen consequences, the two verbs, prompt fragments, a worldgen enrichment hook, spawn
36+
factories, and tests.
37+
38+
## Server Plugin
39+
40+
The plugin exposes `bunnyland_fortunesim.bunnyland_plugins()` and contributes:
41+
42+
- `LuckComponent`, `CharmComponent`, `OmenComponent`, `FortuneToolComponent`.
43+
- `LuckConsequence` — materialises each character's total luck from held charms and active
44+
rituals every tick, emitting `LuckChangedEvent` on band crossings.
45+
- `OmenConsequence` — adds, updates, and clears dynamic room omens driven by loose charms,
46+
leaving sticky worldgen omens alone; emits `OmenSightedEvent` / `OmenClearedEvent`.
47+
- `luck_fragments`, `charm_fragments`, `omen_fragments` — render luck band, held charms, and
48+
room omens into both human and AI prompts.
49+
- `FortuneWorldgenHook` — turns generated charm/trinket objects into `CharmComponent`s and casts
50+
ominous or auspicious generated rooms with a sticky `OmenComponent`.
51+
- `read-fortune` and `ward-luck` — verbs for the seeker (human or AI), emitting `FortuneReadEvent`
52+
and `WardLuckEvent`.
53+
- `spawn_charm`, `spawn_talisman`, `spawn_cursed_trinket`, `spawn_fortune_tool` — spawn factories.
54+
- Good and bad fortune colour a character's mood by reusing the core affect system
55+
(`remember_fortune` attaches a decaying `AffectDelta` thought).
56+
57+
## Running
58+
59+
This package builds no containers. It is loaded into the stock server via `--module`:
60+
61+
```bash
62+
bunnyland serve --module bunnyland_fortunesim
63+
```
64+
65+
`default_enabled=True`, so no `--plugin` flag is required once the module is imported. The
66+
`bunnyland_fortunesim` package must be importable by the server (installed into the server's
67+
environment, or on `PYTHONPATH`).
68+
69+
## Development
70+
71+
Run server tests against a sibling `bunnyland-server` checkout (no install required —
72+
`server/tests/conftest.py` puts both packages on `sys.path`). From `server/`:
73+
74+
```bash
75+
uv run --project ../../bunnyland-server -m pytest
76+
uv run --project ../../bunnyland-server ruff check src tests
77+
```
78+
79+
See [`server/README.md`](server/README.md) for more detail.
80+
81+
## Contributing & Conduct
82+
83+
This plugin follows the Bunnyland project's
84+
[contribution guidelines](CONTRIBUTING.md) and [code of conduct](CODE_OF_CONDUCT.md),
85+
which point back to the `bunnyland-server` repository.
86+
87+
## License
88+
89+
Licensed under the GNU Affero General Public License v3.0. See [LICENSE](LICENSE).

server/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# bunnyland-fortunesim (server plugin)
2+
3+
The out-of-tree Bunnyland plugin package `bunnyland_fortunesim` (plugin id
4+
`bunnyland.fortunesim`).
5+
6+
## Development
7+
8+
Tests run against a sibling `bunnyland-server` checkout without installing anything —
9+
`tests/conftest.py` puts both this package's `src/` and `../bunnyland-server/src` on
10+
`sys.path`. From this `server/` directory:
11+
12+
```bash
13+
# uses the sibling bunnyland-server's virtualenv/deps
14+
uv run --project ../../bunnyland-server -m pytest
15+
# or, if bunnyland + relics are already importable:
16+
python -m pytest
17+
```
18+
19+
Lint:
20+
21+
```bash
22+
uv run --project ../../bunnyland-server ruff check src tests
23+
```
24+
25+
## Loading into the server
26+
27+
```bash
28+
bunnyland serve --module bunnyland_fortunesim
29+
```
30+
31+
`default_enabled=True`, so no `--plugin` flag is required once the module is imported.
32+
33+
## What it contributes
34+
35+
- **Components**`LuckComponent` (an open stat other packs can read), `CharmComponent`,
36+
`OmenComponent`, and `FortuneToolComponent`.
37+
- **Luck**`LuckConsequence` recomputes each character's materialised luck `value` every
38+
tick from `base` luck, the sum of held charms, and any active `ward-luck` ritual bonus (which
39+
expires on its own epoch), emitting `LuckChangedEvent` on band crossings. `luck_fragments`
40+
render the first-person luck-band line ("Fortune favors you today.").
41+
- **Charms & talismans** — a held `CharmComponent` shifts its holder's luck; a negative one is a
42+
cursed trinket. `charm_fragments` render the first-person "you carry a lucky …" line, and
43+
`spawn_charm` / `spawn_talisman` / `spawn_cursed_trinket` make the items.
44+
- **Omens**`OmenConsequence` casts a room foreboding (a cursed charm loose on the floor) or
45+
auspicious (a lucky one), picking the omen line deterministically and clearing it when the
46+
charms leave. Sticky worldgen omens are left untouched. `omen_fragments` render the room's
47+
omen; `OmenSightedEvent` / `OmenClearedEvent` fire on change.
48+
- **Fortune-telling** — the `read-fortune` verb over a held tarot/tea-leaves tool assembles a
49+
deterministic reading (biased upward by the seeker's luck and weaving in the room omen) and
50+
emits a private `FortuneReadEvent`. `spawn_fortune_tool` makes the tool.
51+
- **Superstitions** — the `ward-luck` verb (knock-on-wood, toss-salt, cross-fingers) grants a
52+
temporary luck boost, granting a `LuckComponent` on the fly if the character lacks one, and
53+
emits `WardLuckEvent`.
54+
- **Mood reuse** — good/bad fortune colours a character's mood by reusing the core affect
55+
system: `remember_fortune` attaches a decaying `ThoughtComponent` carrying an `AffectDelta`,
56+
which the stock `AffectAggregation` folds into the character's `AffectComponent`.
57+
- **Worldgen**`FortuneWorldgenHook` tags generated charm/trinket objects with
58+
`CharmComponent` and casts ominous/auspicious generated rooms with a sticky `OmenComponent`.
59+
60+
## Determinism
61+
62+
Every luck-biased outcome and fortune reading comes from a `hashlib` digest of stable ids plus
63+
the world `epoch`, reduced over sorted tables (`bands.digest_unit`, `bands.biased_index`,
64+
`bands.luck_multiplier`). No `random`, no wall-clock time — results are stable across machines
65+
and across `PYTHONHASHSEED`.

server/pyproject.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[project]
2+
name = "bunnyland-fortunesim"
3+
version = "0.1.0"
4+
description = "Out-of-tree Bunnyland plugin: an expansion-pack-sized bundle of luck, charms, omens, fortune-telling, and superstitions."
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"bunnyland",
9+
"pydantic>=2.0",
10+
"relics",
11+
]
12+
13+
[dependency-groups]
14+
dev = [
15+
"pytest>=8.0",
16+
]
17+
18+
[build-system]
19+
requires = ["hatchling"]
20+
build-backend = "hatchling.build"
21+
22+
[tool.hatch.build.targets.wheel]
23+
packages = ["src/bunnyland_fortunesim"]
24+
25+
[tool.pytest.ini_options]
26+
testpaths = ["tests"]
27+
28+
[tool.ruff]
29+
line-length = 100
30+
target-version = "py312"
31+
32+
[tool.ruff.lint]
33+
select = ["E", "F", "I", "UP", "B"]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Out-of-tree Bunnyland plugin: luck, charms, omens, fortune-telling, and superstitions.
2+
3+
An expansion-pack-sized themed bundle. Luck is a stat other packs can read; charms and
4+
talismans shift it while held; omens colour rooms foreboding or auspicious; ``read-fortune``
5+
surfaces a deterministic reading; and ``ward-luck`` rituals buy a temporary luck boost. Every
6+
luck-biased outcome and fortune reading is derived from a hash of stable ids plus the world
7+
epoch, so results are deterministic and never use runtime randomness.
8+
"""
9+
10+
from .bands import (
11+
BLESSED,
12+
CURSED,
13+
EVEN,
14+
LUCKY,
15+
UNLUCKY,
16+
biased_index,
17+
digest_unit,
18+
luck_band,
19+
luck_multiplier,
20+
)
21+
from .charms import (
22+
charm_fragments,
23+
held_charm_bonus,
24+
spawn_charm,
25+
spawn_cursed_trinket,
26+
spawn_talisman,
27+
)
28+
from .components import (
29+
AUSPICIOUS,
30+
FOREBODING,
31+
CharmComponent,
32+
LuckComponent,
33+
OmenComponent,
34+
)
35+
from .enrichment import FortuneWorldgenHook
36+
from .fortune import (
37+
FortuneReadEvent,
38+
FortuneToolComponent,
39+
ReadFortuneHandler,
40+
compose_reading,
41+
spawn_fortune_tool,
42+
)
43+
from .install import install_fortunesim, install_fortunesim_omens
44+
from .luck import (
45+
LuckChangedEvent,
46+
LuckConsequence,
47+
effective_luck,
48+
fortune_mood,
49+
luck_fragments,
50+
remember_fortune,
51+
)
52+
from .omens import (
53+
OmenClearedEvent,
54+
OmenConsequence,
55+
OmenSightedEvent,
56+
omen_fragments,
57+
)
58+
from .plugin import PLUGIN_ID, bunnyland_plugins, plugin
59+
from .spatial import holder_of, room_of
60+
from .superstition import WardLuckEvent, WardLuckHandler
61+
62+
__all__ = [
63+
"AUSPICIOUS",
64+
"BLESSED",
65+
"CURSED",
66+
"EVEN",
67+
"FOREBODING",
68+
"LUCKY",
69+
"PLUGIN_ID",
70+
"UNLUCKY",
71+
"CharmComponent",
72+
"FortuneReadEvent",
73+
"FortuneToolComponent",
74+
"FortuneWorldgenHook",
75+
"LuckChangedEvent",
76+
"LuckComponent",
77+
"LuckConsequence",
78+
"OmenClearedEvent",
79+
"OmenComponent",
80+
"OmenConsequence",
81+
"OmenSightedEvent",
82+
"ReadFortuneHandler",
83+
"WardLuckEvent",
84+
"WardLuckHandler",
85+
"biased_index",
86+
"bunnyland_plugins",
87+
"charm_fragments",
88+
"compose_reading",
89+
"digest_unit",
90+
"effective_luck",
91+
"fortune_mood",
92+
"held_charm_bonus",
93+
"holder_of",
94+
"install_fortunesim",
95+
"install_fortunesim_omens",
96+
"luck_band",
97+
"luck_fragments",
98+
"luck_multiplier",
99+
"omen_fragments",
100+
"plugin",
101+
"remember_fortune",
102+
"room_of",
103+
"spawn_charm",
104+
"spawn_cursed_trinket",
105+
"spawn_fortune_tool",
106+
"spawn_talisman",
107+
]

0 commit comments

Comments
 (0)