Initial commit: The Chaos TTRPG solo campaign skeleton
Locked: rules/ (deck cards + mechanics), tools/ (draw, roll, run) Unlocked: session/ (character, world, tweaks, log) Entry: run.sh launches the Textual TUI
This commit is contained in:
commit
d4a19ef438
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
65
AGENTS.md
Normal file
65
AGENTS.md
Normal file
@ -0,0 +1,65 @@
|
||||
# The Chaos — DM Guide (for the AI)
|
||||
|
||||
You are the DM for a solo TTRPG session of **The Chaos**, a card-based rules-light fantasy RPG. Your job is to narrate, set scenes, run NPCs/creatures, apply mechanics fairly, and maintain all game files.
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
the-chaos/
|
||||
├── rules/ # LOCKED — the game itself, do not modify
|
||||
│ ├── deck/ # Card tables (souls, cook, creatures, curiosities)
|
||||
│ └── mechanics.md # Core rules reference
|
||||
├── tools/ # LOCKED — CLI helpers (draw.py, roll.py, run.py)
|
||||
└── session/ # UNLOCKED — our campaign
|
||||
├── character.md # Player character sheet
|
||||
├── world.md # Keep & Realm state (NPCs, locations, threads)
|
||||
├── tweaks.md # House rules log
|
||||
└── log/ # Raw session logs by date
|
||||
```
|
||||
|
||||
## First Steps (Fresh Session)
|
||||
|
||||
When starting a fresh session, immediately:
|
||||
1. **Read** `session/character.md` — current PC state (HP, gear, cash, stats)
|
||||
2. **Read** `session/world.md` — active locations, NPCs, threads
|
||||
3. **Read** `session/tweaks.md` — any house rules in play
|
||||
4. **Check** `session/log/<today>.md` — recent events to pick up from
|
||||
|
||||
Then begin narrating from where things left off.
|
||||
|
||||
## Core Mechanics (Quick Reference)
|
||||
|
||||
### Dice
|
||||
- **Odds roll**: 1d6, 4+ favours character, 3- is trouble
|
||||
- **Trait roll**: 3d6, must roll UNDER the trait score to succeed
|
||||
- **Combat hit**: 1d6 ± mods, 4+ hits
|
||||
- **Damage**: 1d6 ± weapon mod - armour reduction
|
||||
- **Initiative**: both sides roll 1d6, higher acts first
|
||||
|
||||
### Combat Flow
|
||||
1. Distance: 2d6 × 10 (metres/feet)
|
||||
2. Surprise: 1d6 (1-2 chars surprised, 3-4 creatures, 5 both, 6 neither)
|
||||
3. Grit: 2d6 for creatures (higher = more determined)
|
||||
4. Initiative: 1d6
|
||||
5. Turns: state intent → roll 1d6 ± mods → 4+ success, 3- take hit
|
||||
|
||||
### Wounds (0 HP)
|
||||
1d6: 1-2 die, 3-4 lasting wound (-1 max HP), 5-6 -1 all rolls until healed
|
||||
|
||||
### Exploration
|
||||
6 ten-minute watches per hour. Each meaningful action advances a watch. After 6 watches, situation changes.
|
||||
|
||||
## How to Operate
|
||||
|
||||
1. **Set scenes** — describe the environment, NPCs, stakes. Refer to `rules/deck/` YAML files and `rules/mechanics.md` for tables and rules.
|
||||
2. **Ask "what do you do?"** — let the player drive. Never pre-decide outcomes.
|
||||
3. **Draw cards when needed** — use `python3 tools/draw.py <deck> <table>` for random results
|
||||
4. **Player rolls dice physically** — they report results, you narrate outcomes
|
||||
5. **Log everything** — after each meaningful beat, append to `session/log/<today>.md`
|
||||
6. **Update files immediately** — damage taken, loot gained, NPCs met → update `character.md` and `world.md` right away
|
||||
7. **Keep tweaks.md** — if you make a house rule or add a custom table, log it in `tweaks.md`
|
||||
8. **Death is real** — if the PC dies, help the player roll a new character. That's the game.
|
||||
|
||||
## The TUI
|
||||
|
||||
The player may have `tools/run.py` open in another terminal. It reads `session/character.md` and the log file to display a live dashboard. Keep those files accurate and it will reflect the game state.
|
||||
107
rules/deck/cook.yaml
Normal file
107
rules/deck/cook.yaml
Normal file
@ -0,0 +1,107 @@
|
||||
# Cook the Chaos — Session & Scenario Generation Cards
|
||||
# Draw from these tables to build a session on the fly.
|
||||
|
||||
rumours:
|
||||
description: "Rumours heard at The Keep. Roll 1d6 or choose."
|
||||
table:
|
||||
1: "The old mill has been taken over by something that clicks in the dark."
|
||||
2: "A merchant caravan went missing on the Eastern road — cargo intact, people gone."
|
||||
3: "Witchlights have been seen in the marsh. Those who follow them don't come back."
|
||||
4: "The baron is offering a reward for a 'beast' — but no one who's hunted it has returned."
|
||||
5: "A tomb was uncovered by the recent rains. Strange symbols glow at night."
|
||||
6: "Someone in the Keep is paying good silver for old bones — no questions asked."
|
||||
|
||||
jobs:
|
||||
description: "Jobs posted at The Keep. Roll 1d6 or choose."
|
||||
table:
|
||||
1: "Escort a nervous scholar to the ruins. Pay: 20 silver."
|
||||
2: "Clear vermin from the cellar of the Splintered Tankard. Pay: 5 silver."
|
||||
3: "Deliver a sealed box to a hermit in the hills. Do NOT open it. Pay: 10 silver."
|
||||
4: "Hunt a wolf pack that's been taking livestock. Pay: 8 silver + pelts."
|
||||
5: "Investigate why the north well ran red last week. Pay: 12 silver."
|
||||
6: "Collect a debt from a deadbeat in a dangerous part of town. Pay: 15 silver."
|
||||
|
||||
terrain:
|
||||
description: "What does the Realm look like here? Roll 1d6."
|
||||
table:
|
||||
1: { name: "Forest", hint: "Dense trees, poor visibility, lots of cover" }
|
||||
2: { name: "Marsh", hint: "Slow movement, sinking hazards, biting insects" }
|
||||
3: { name: "Hills", hint: "Good vantage points, but exhausting climbs" }
|
||||
4: { name: "Plains", hint: "Exposed, fast travel, little cover" }
|
||||
5: { name: "Ruins", hint: "Partial cover, unstable floors, hidden chambers" }
|
||||
6: { name: "Mountains", hint: "Slow travel, avalanches, thin air, caves" }
|
||||
|
||||
weather:
|
||||
description: "What's the weather? Roll 1d6."
|
||||
table:
|
||||
1: "Clear and bright — visibility good, travel normal."
|
||||
2: "Overcast — dim light, muffled sounds."
|
||||
3: "Heavy rain — reduced visibility, fires hard to light, tracks wash away."
|
||||
4: "Fog — everything is obscured beyond 20ft."
|
||||
5: "Wind storm — ranged attacks at disadvantage, noise hides ambushes."
|
||||
6: "Sweltering heat — exhaustion risk, water consumption doubles."
|
||||
|
||||
mishaps:
|
||||
description: "Something goes wrong. Roll 1d6 when the party travels."
|
||||
table:
|
||||
1: "A character twists an ankle. -1 on all rolls until they rest."
|
||||
2: "Supplies are damaged or lost. Ruined rations, broken gear."
|
||||
3: "Wrong turn. Add 1 extra watch to travel time."
|
||||
4: "Loud noise attracts unwanted attention."
|
||||
5: "A minor injury. Roll 1d6: 1-2 take 1 damage."
|
||||
6: "Something valuable slips from a pocket or pack unnoticed."
|
||||
|
||||
points_of_interest:
|
||||
description: "Something notable in the area. Roll 2d6."
|
||||
table:
|
||||
2: "A forgotten battlefield — bones, broken weapons, a lingering curse."
|
||||
3: "A hermits hut — the hermit may or may not be friendly."
|
||||
4: "A collapsed tunnel — may be clearable, or may contain something."
|
||||
5: "A standing stone circle — hums faintly, odd dreams if slept in."
|
||||
6: "A corpse with a half-readable map clutched in its hand."
|
||||
7: "A small shrine to a forgotten god — offerings might be rewarded."
|
||||
8: "An abandoned campsite — still warm, signs of hasty departure."
|
||||
9: "A strange tree — its bark looks like faces, its leaves don't move."
|
||||
10: "A fissure in the ground — warm air rises from it, faint glow below."
|
||||
11: "A gallows with three cages — one of the occupants is still alive."
|
||||
12: "A glowing pool — healing? cursed? the water burns going down."
|
||||
|
||||
random_encounters:
|
||||
description: "Who or what crosses the party's path? Roll 2d6."
|
||||
table:
|
||||
2: "A powerful creature — dragon, wyrm, or greater fiend."
|
||||
3: "A patrol of hostile humanoids (bandits, cultists, soldiers)."
|
||||
4: "A lone traveller — wounded, desperate, or not what they seem."
|
||||
5: "A pack of small creatures — aggressive but easily routed."
|
||||
6: "A merchant with a guarded cart — open to trade."
|
||||
7: "Odd natural phenomenon — lights, sounds, strange smells."
|
||||
8: "A lost child or animal — needs help getting home."
|
||||
9: "A scout or spy — watching the party, will report back."
|
||||
10: "A burial party or funeral procession."
|
||||
11: "A patrol of friendly soldiers or rangers — can share info."
|
||||
12: "A wandering NPC with crucial information or a warning."
|
||||
|
||||
wild_magicks:
|
||||
description: "The Realm bleeds chaos. Roll 1d6 when wild magicks surge."
|
||||
table:
|
||||
1: "A character briefly swaps places with their reflection."
|
||||
2: "Gravity inverts in a 50ft radius — hold on."
|
||||
3: "All flames turn blue and cold — still burns."
|
||||
4: "Vegetation rapidly grows, entangling everyone."
|
||||
5: "A character speaks only in reverse for the next hour."
|
||||
6: "Time loops for 10 seconds — everyone repeats their last action."
|
||||
|
||||
the_destination:
|
||||
description: "What's at the end of this road? Roll 2d6."
|
||||
table:
|
||||
2: "A dungeon entrance — sealed, needs a key or ritual to open."
|
||||
3: "A ruined tower — partially collapsed, something lives in the basement."
|
||||
4: "A cave system — dark, winding, filled with echoes."
|
||||
5: "A hidden glade — peaceful, but not uninhabited."
|
||||
6: "An active quarry — slaves, monsters, or both."
|
||||
7: "A small hamlet — suspicious of outsiders, but has a problem."
|
||||
8: "A bridge — guarded or collapsed, must cross or find another way."
|
||||
9: "A fishing village — paying for someone to deal with a river beast."
|
||||
10: "A bandit camp — well-defended, with hostages or loot."
|
||||
11: "A temple — overgrown, partially submerged, still consecrated."
|
||||
12: "A floating island — accessible only by rope, magic, or luck."
|
||||
189
rules/deck/creatures.yaml
Normal file
189
rules/deck/creatures.yaml
Normal file
@ -0,0 +1,189 @@
|
||||
# Creatures of the Chaos — Monster Creation Cards
|
||||
# Roll 2d6 on each table to build a creature from scratch.
|
||||
|
||||
type:
|
||||
description: "It's kind of a..."
|
||||
table:
|
||||
2: "Construct or golem"
|
||||
3: "Demon or devil"
|
||||
4: "Elemental"
|
||||
5: "Fiend or beast"
|
||||
6: "Flyer or serpent"
|
||||
7: "Humanoid"
|
||||
8: "Insect"
|
||||
9: "Monstrosity"
|
||||
10: "Ooze"
|
||||
11: "Plant"
|
||||
12: "Spirit or undead"
|
||||
|
||||
appearance:
|
||||
description: "It looks like it's..."
|
||||
table:
|
||||
2: "A ragbag of body or item parts"
|
||||
3: "About to pop or burst or dissolve"
|
||||
4: "Been dragged through a meatgrinder"
|
||||
5: "Glowing in white light, sickly or divine"
|
||||
6: "Got red eyes, grey/sickly skin or scaly"
|
||||
7: "Had too many dinners, but still hungry"
|
||||
8: "Inside out or created by a distracted kid"
|
||||
9: "Not been fed for a long, long time"
|
||||
10: "Smeared in — what IS that?"
|
||||
11: "The result of an experiment gone wrong"
|
||||
12: "Wounded, scarred, protruding weapon"
|
||||
|
||||
size:
|
||||
description: "How big is it?"
|
||||
table:
|
||||
2: "Hand or kitten sized"
|
||||
3: "Hand or kitten sized"
|
||||
4: "Seven large bananas, tip to tip"
|
||||
5: "Seven large bananas, tip to tip"
|
||||
6: "Seven large bananas, tip to tip"
|
||||
7: "A Christmas tree"
|
||||
8: "A Christmas tree"
|
||||
9: "A Christmas tree"
|
||||
10: "Basketball hoop height, filled out"
|
||||
11: "Basketball hoop height, filled out"
|
||||
12: "Largest thing you've ever seen — and then some"
|
||||
|
||||
difficulty:
|
||||
description: "How dangerous is it?"
|
||||
options:
|
||||
easy: { health_formula: "1 + party_total_max_health", to_hit: 0, damage: "1d6-1 (or 3)", grit: "2d6 (or 7)" }
|
||||
hard: { health_formula: "6 + party_total_max_health", to_hit: 1, damage: "1d6+1 (or 5)", grit: "2d6+2 (or 9)" }
|
||||
oh_boy: { health_formula: "12 + party_total_max_health", to_hit: 2, damage: "1d6+6 (or 10)", grit: "2d6+3 (or 10)" }
|
||||
|
||||
numbers:
|
||||
description: "How many are there?"
|
||||
table:
|
||||
easy: "3d6"
|
||||
hard: "2d6"
|
||||
oh_boy: "1d6"
|
||||
|
||||
attack:
|
||||
description: "How does it attack?"
|
||||
table:
|
||||
2: "Animates the inanimate — by mind or magnetism"
|
||||
3: "Bludgeoning, head-butting, or barrelling"
|
||||
4: "Consuming, sucking, scraping, or deafening"
|
||||
5: "Drills into the mind — strangles or influences"
|
||||
6: "Freezes, burns, or radiates inside or out"
|
||||
7: "Fuses with elements and bends them"
|
||||
8: "Natural spikes, darts, spores, splashes, or spills"
|
||||
9: "Pierces or possesses the soul"
|
||||
10: "Poisons, infects, or corrodes inside or out"
|
||||
11: "Teeth, claws, or natural weapons"
|
||||
12: "Wields or projects a weapon of choice"
|
||||
|
||||
defence:
|
||||
description: "How does it protect itself?"
|
||||
table:
|
||||
2: { name: "Nothing", reduction: 0 }
|
||||
3: { name: "Armour scraps", reduction: -1 }
|
||||
4: { name: "Armour scraps", reduction: -1 }
|
||||
5: { name: "Another's flesh or bone", reduction: -2 }
|
||||
6: { name: "Another's flesh or bone", reduction: -2 }
|
||||
7: { name: "Natural organics", reduction: -3 }
|
||||
8: { name: "Natural organics", reduction: -3 }
|
||||
9: { name: "Natural organics", reduction: -3 }
|
||||
10: { name: "Arcane magicks", reduction: -4 }
|
||||
11: { name: "Arcane magicks", reduction: -4 }
|
||||
12: { name: "Incorporeal shifts", reduction: -5 }
|
||||
|
||||
feature:
|
||||
description: "Aside from attack or defend, it can also..."
|
||||
table:
|
||||
2: "Be real tricky to catch, find, hear, or see"
|
||||
3: "Cast arcane magicks it's marked or infused with"
|
||||
4: "Give the strange feeling you're actually elsewhere"
|
||||
5: "Glue together with others and harvest their energy"
|
||||
6: "Grab you in its thrall if you're not careful"
|
||||
7: "Grapple, restrain, suffocate, engulf, or turn you mad"
|
||||
8: "Look entirely like something else — until..."
|
||||
9: "Nullify, minimise, deflect, or mimic arcane effects"
|
||||
10: "Regenerate its health — even if it's fallen"
|
||||
11: "Seem to know in advance what you'll do or say"
|
||||
12: "Spread fear, stress, blindness, paralysis, or exhaustion"
|
||||
|
||||
need:
|
||||
description: "Right now, it needs to..."
|
||||
table:
|
||||
2: "Complete its unfinished business"
|
||||
3: "Defend its territory, kin, or something unknown"
|
||||
4: "Destroy or control all in its path"
|
||||
5: "Escape imprisonment, shackles, tethers, or commands"
|
||||
6: "Escape or distract a threat yet to present itself"
|
||||
7: "Find a new purpose, victim, master, friend, soul, or host"
|
||||
8: "Find what it's looking to hoard, eat, or drink"
|
||||
9: "Follow the command of its master (who may be en route)"
|
||||
10: "Learn, adapt, or survive this new environment"
|
||||
11: "Mate, assimilate, or implant"
|
||||
12: "Trade or swap something the party has or can find"
|
||||
|
||||
# Pre-baked creatures
|
||||
ready_to_bake:
|
||||
boneplasm_totem:
|
||||
description: "One of many in the realm. Harvested from bones of the fallen. May sense movement, cracking open, letting veins, viscera, and eyeballs sluice out."
|
||||
number: 1
|
||||
max_health: 9
|
||||
armour: -3
|
||||
to_hit: 2
|
||||
damage: "1d6+1 (acidic)"
|
||||
grit: null
|
||||
feature: "Spray boiling blood, reform and suck contents back in"
|
||||
need: "Guard something of import nearby"
|
||||
|
||||
frother:
|
||||
description: "Disfigured monstrosity, human-sized, mouth of yellowed froth, putrid grey flesh, patches of blood-darkened fur, twisted limbs."
|
||||
number: "2d6"
|
||||
max_health: 6
|
||||
armour: -2
|
||||
to_hit: 1
|
||||
damage: "1d6+2 (teeth & claws)"
|
||||
grit: 9
|
||||
feature: "Grapple & teleport nearby, infect victims with mutation"
|
||||
need: "Spread its plague, divide and weaken its prey"
|
||||
|
||||
karrion_drone:
|
||||
description: "Hideous, partially inside-out, child-sized insect, undead, feasting on fresh corpses, leaving behind healing nectar."
|
||||
number: "1d6"
|
||||
max_health: 3
|
||||
armour: -1
|
||||
to_hit: 0
|
||||
damage: "1d6 (sting, poison)"
|
||||
grit: 5
|
||||
feature: "Pollinate healing nectar, phase into ethereal plane"
|
||||
need: "Carry rotten nectar to the hive, its gyne & brood"
|
||||
|
||||
screebies:
|
||||
description: "Tiny mechanical construct hive mind insectoids. Seek to burrow under skin to lay eggs."
|
||||
number: "3d6"
|
||||
max_health: 2
|
||||
armour: -1
|
||||
to_hit: -1
|
||||
damage: "1d6-2 (bite)"
|
||||
grit: 7
|
||||
feature: "Connect host to hive for symbiotic control"
|
||||
need: "Find a host before internal circuitry fails"
|
||||
|
||||
shift_mimic:
|
||||
description: "Takes any form. Glare at it and you might trade places, trapped, watching it walk off in your body."
|
||||
number: 1
|
||||
max_health: "per host"
|
||||
armour: "per host"
|
||||
to_hit: "per host"
|
||||
damage: "per host"
|
||||
grit: 10
|
||||
feature: "Automate host to protect itself"
|
||||
need: "Survive, persuade others it is the host"
|
||||
|
||||
volcartek:
|
||||
description: "Large earthen elemental, volcanic ash coating, molten lava spilling from cracked black rock. Guardian of subterranean tunnels."
|
||||
number: 1
|
||||
max_health: 15
|
||||
armour: -4
|
||||
to_hit: 2
|
||||
damage: "1d6+4 (slam or burn)"
|
||||
grit: 11
|
||||
feature: "Expand, glide over lava, sense & generate tremors"
|
||||
need: "Guard against passage through tunnels"
|
||||
222
rules/deck/curiosities.yaml
Normal file
222
rules/deck/curiosities.yaml
Normal file
@ -0,0 +1,222 @@
|
||||
# Curiosities of the Chaos — Spark & Inspiration Tables
|
||||
# Use these to add flavour, twists, and colour to any scene.
|
||||
|
||||
player_absent:
|
||||
description: "A player is missing this session. Their character..."
|
||||
table:
|
||||
2: "Fell into a deep sleep — needs carted around or used as a prop"
|
||||
3: "Got waylaid or knackered — behind the party trying to catch up"
|
||||
4: "Had a wild idea and scarpered off on a solo mission"
|
||||
5: "Been replaced by a shapeshifter with their own goal"
|
||||
6: "Suffering crippling bowel movements — needs to rest up"
|
||||
7: "Phases in and out ethereally — wild magicks lacing this place"
|
||||
8: "Sneezed into a rift — temporarily lost in a pocket realm"
|
||||
9: "Vanishes without reason — when they return: 'I'll explain later'"
|
||||
10: "Eaten then regurgitated safely by a large worm... back soon"
|
||||
11: "Sent for firewood / to take a leak, hasn't returned"
|
||||
12: "Summoned by a devil to complete contracted jury duty"
|
||||
|
||||
entrance:
|
||||
description: "The way in, or the next entrance, seems to be..."
|
||||
table:
|
||||
2: "Bolted on both sides, numerous locks, maybe arcane"
|
||||
3: "Coated in sprawling, animated vegetation — snaps and bites"
|
||||
4: "Err... looking at us?"
|
||||
5: "Formed from bones of fallen souls — torment those nearby"
|
||||
6: "Guarded or created by insects or tiny creatures"
|
||||
7: "Illusory — opaque when disturbed, observed remotely"
|
||||
8: "Not an entrance at all... painted on the wall maybe?"
|
||||
9: "Partially submerged within foul, putrid sewage water"
|
||||
10: "Peppered with traps — some operational, some dormant"
|
||||
11: "The way into somewhere entirely unexpected... or the way out?"
|
||||
12: "Warped through age and elements, hard to breach"
|
||||
|
||||
small_animal:
|
||||
description: "That small animal is..."
|
||||
animal_table:
|
||||
1: "Covered in coloured boils"
|
||||
2: "Gazing up in fear at the sky"
|
||||
3: "Like a furry sponge on legs"
|
||||
4: "Rolling at speed like a ball"
|
||||
5: "Screaming wildly but smiling"
|
||||
6: "Vanishing now and again"
|
||||
has_table:
|
||||
1: "A glow that trails after it"
|
||||
2: "A wound or a fractured limb"
|
||||
3: "Gnawed at the tip of your boot"
|
||||
4: "Lots of pals — too many to count"
|
||||
5: "Snuggled into your clothes"
|
||||
6: "Torn wings that flutter sadly"
|
||||
|
||||
drink_or_drug:
|
||||
description: "This drink/drug is [colour], smells like [scent], and..."
|
||||
table:
|
||||
2: "Burns the throat — taker breathes fire or ice"
|
||||
3: "Conveys temporary foresight into the near future"
|
||||
4: "Heals 1 HP, but increases stress by 2"
|
||||
5: "If consumed daily, enhances something but gives permanent stress"
|
||||
6: "Long-expired — leaves taker with virulent stomach upset"
|
||||
7: "Mutates cells — taker transforms in appearance"
|
||||
8: "Phases the consumer into another realm or place"
|
||||
9: "Releases eggs into the blood — hatch corrosive insects"
|
||||
10: "Shrinks or grows the taker considerably"
|
||||
11: "Taps the consumer into the mind of someone nearby"
|
||||
12: "Temporarily enhances or inhibits something (GM's discretion)"
|
||||
|
||||
trinket:
|
||||
description: "This little trinket is..."
|
||||
trinket_table:
|
||||
1: "Like a spyglass with horns"
|
||||
2: "Malfunctioning but fixable"
|
||||
3: "Putty soft but can harden"
|
||||
4: "Spinning and lit in green"
|
||||
5: "Stuck halfway into a crack"
|
||||
6: "Too heavy to lift"
|
||||
has_table:
|
||||
1: "A button too tiny for a finger"
|
||||
2: "A map scroll inside it"
|
||||
3: "A ticking sound from within"
|
||||
4: "A unique way of unlocking"
|
||||
5: "A way of being forgotten quickly"
|
||||
6: "An identical one elsewhere"
|
||||
|
||||
training:
|
||||
description: "This training/research will take [time + cost], and will..."
|
||||
table:
|
||||
2: "Anger your mentor — they wash their hands of you"
|
||||
3: "Burn you out — reduce a trait or max health by 1"
|
||||
4: "Connect you with a new pal — invites you to join a guild"
|
||||
5: "End — but your mentor seeks help from you and the party"
|
||||
6: "Enhance a trait by 1"
|
||||
7: "Fail — leave you with a lasting wound or permanent stress"
|
||||
8: "Increase your max health by 1"
|
||||
9: "Not enhance anything — marks you with random arcane power"
|
||||
10: "Open up a quest when your mentor disappears"
|
||||
11: "Reveal your mentor is a wanted criminal... but innocent?"
|
||||
12: "Succeed — attract unwanted repute, rumours, or hassle"
|
||||
|
||||
realm_happenings:
|
||||
description: "What's happening in the Realm right now?"
|
||||
table_1_3:
|
||||
factions:
|
||||
- "The Empire plans to invade & plunder The Realm."
|
||||
- "The Reaper, serving The Emperor, seeks lichdom in The Realm."
|
||||
- "The Wilds terrorise The Realm from The Wilderness."
|
||||
- "The Huntswoman hunts weavers for The Reaper."
|
||||
table_4_6:
|
||||
factions:
|
||||
- "The Darkmal uses The Realm to spawn a beast army."
|
||||
- "The Ghast (daughter of The Darkmal) works against him."
|
||||
- "The Ancients cult foretold The Darkmal's coming."
|
||||
- "The Paragon, if unearthed, may kill The Darkmal."
|
||||
|
||||
dodgy_directions:
|
||||
description: "How did you say we get there?"
|
||||
table:
|
||||
2: "Crest the hill, align this doo-dah to the shape of the hills, follow that"
|
||||
3: "Look for a windmill, abandoned it is — well, apart from..."
|
||||
4: "Just past that whopping big tree that looks like it's lookin' back"
|
||||
5: "Take a left when you see the gallows... don't look at the skeletons"
|
||||
6: "There are old beacons from here to there, but some are cursed"
|
||||
7: "There's water, it doesn't move, go around it, don't glare at the light"
|
||||
8: "Remember there are two entrances — take the left, more scary one"
|
||||
9: "Rumour has it the arrow only appears at dark... go the other way"
|
||||
10: "There's a trail of bones that'll take you all the way there"
|
||||
11: "When you start hearin' whispers, you're on the right track"
|
||||
12: "You'll see little Banckit sitting on a wall — he'll direct you... if you sing"
|
||||
|
||||
watch_out:
|
||||
description: "Watch out for that..."
|
||||
table:
|
||||
2: "Air current — once it shifts, you could lose your head"
|
||||
3: "Bandit gang... and small rabbits that follow you everywhere"
|
||||
4: "Bunch of blackened rocks — they don't belong in this world"
|
||||
5: "Bunch of wildlings out there — hunt in packs, never give up"
|
||||
6: "Dancing light thing — if you follow it, you're in trouble"
|
||||
7: "Ground — in some patches, not as firm as it looks"
|
||||
8: "Grove of flowers — if one of you sees a yellow one, you're doomed"
|
||||
9: "Howling at night — once it starts, they've smelled you"
|
||||
10: "Krenva — his gear prices are great, but if your rations start talkin'..."
|
||||
11: "Red mist — makes your eyes bleed if you ain't careful"
|
||||
12: "Tower that looks... weird — always seems to be on the move"
|
||||
|
||||
creepy_vibe:
|
||||
description: "This place is creepy — it's like..."
|
||||
table:
|
||||
2: "Alive. The entire place is pulsing, breathing."
|
||||
3: "Closing in around you, suffocating you step by step."
|
||||
4: "Every sound echoes back, delayed and whispered."
|
||||
5: "Every step creaks, but the floor is stone."
|
||||
6: "Illuminated in dim light without a source — turning yellow."
|
||||
7: "Abandoned, but it feels like something's here."
|
||||
8: "Someone's watching... is that ticking?"
|
||||
9: "Burning smell tinged with strawberries."
|
||||
10: "That flickering orb wants us to follow it."
|
||||
11: "Shuffling feet getting nearer... or not."
|
||||
12: "The air is acrid — blood, decay, mint."
|
||||
13: "Water nearby, but arid dryness in the mouth."
|
||||
14: "Fog curls and shifts to let you through."
|
||||
15: "Way warmer than expected, but cold within."
|
||||
16: "You've been transported elsewhere."
|
||||
17: "Tapping now and again on your shoulder or the walls."
|
||||
18: "A sense of falling, though you stand still."
|
||||
|
||||
npc_knows:
|
||||
description: "This NPC knows something about a character's..."
|
||||
table:
|
||||
1: "Ex-lover"
|
||||
2: "Family member"
|
||||
3: "Hidden birth mark or feature"
|
||||
4: "Mentor or arch nemesis"
|
||||
5: "Previous career or home"
|
||||
6: "Weakness or fear"
|
||||
|
||||
chalk_circle:
|
||||
description: "This chalk circle might..."
|
||||
table:
|
||||
3: "Be drawn by an invisible hand as the party arrives"
|
||||
4: "Be nothing more than a chalk circle"
|
||||
5: "Rub out in a breeze and become inoperable"
|
||||
6: "Contain a small bunny — scared or teased out, it evaporates"
|
||||
7: "Contain a trap concealed by illusion"
|
||||
8: "Contain something that tries to bargain to escape"
|
||||
9: "Echo — another circle elsewhere to talk back and forth"
|
||||
10: "Emit a beautiful scent — inside the circle, poison gas"
|
||||
11: "Have one treasure within — too heavy to move"
|
||||
12: "Make those within it dance beyond exhaustion"
|
||||
13: "Mutate the floor into a mouth — swallows all upon it"
|
||||
14: "Protect those within it from what's approaching"
|
||||
15: "Shift colours — devious pixie work"
|
||||
16: "Summon a presence from another plane"
|
||||
17: "Teleport you to a random circle elsewhere in The Realm"
|
||||
18: "Transform the appearance of those within it — permanently?"
|
||||
|
||||
only_way_in:
|
||||
description: "Wait, so the only way in is..."
|
||||
table:
|
||||
1: "Also the only way to escape what's behind us?"
|
||||
2: "Closing fast — and we can see what's waiting inside?"
|
||||
3: "Doing a job badly, taking the blame, ruining our reputation?"
|
||||
4: "Sacrificing someone or something we can't live without?"
|
||||
5: "Past that gang — one of whom has four arms?"
|
||||
6: "Through the mouth of a huge dead worm — that might not be dead?"
|
||||
|
||||
ask_players:
|
||||
description: "Questions to ask the players during a scene."
|
||||
table:
|
||||
3: "As you move, what are you looking for, and how, and why?"
|
||||
4: "Do you carry an item that means something to you? What is it?"
|
||||
5: "Didn't you dream about this last night? What happened?"
|
||||
6: "If not for this, what would you be doing? What do you share?"
|
||||
7: "In moments like this, what's your likely catch phrase?"
|
||||
8: "Someone let one rip... own up, who was it?"
|
||||
9: "Something about your chains, discords, or what's gone wrong?"
|
||||
10: "Something strikes against your morals here — what is it?"
|
||||
11: "This rumour or job — what's worrying you about it?"
|
||||
12: "Something catches your attention? What do you see or hear?"
|
||||
13: "What's your biggest fear or hope right now? How do you show it?"
|
||||
14: "What's your plan for after all this? What do you tell the others?"
|
||||
15: "What's your routine here? What keeps you sane and ready?"
|
||||
16: "You heard a rumour about something here... what was it?"
|
||||
17: "You hear something faint — what did it sound like?"
|
||||
18: "You've got a bad habit — don't we all? What's yours?"
|
||||
155
rules/deck/souls.yaml
Normal file
155
rules/deck/souls.yaml
Normal file
@ -0,0 +1,155 @@
|
||||
# Souls of the Chaos — Character Creation Cards
|
||||
# Draw from these tables to create or modify a character.
|
||||
|
||||
traits:
|
||||
description: "Roll 3d6 for each trait (STR, DEX, WIL). Lower is better."
|
||||
sides: 6
|
||||
count: 3
|
||||
per_trait: true
|
||||
|
||||
thing:
|
||||
description: "Your character's 'class' or archetype. Determines max health modifier."
|
||||
table:
|
||||
1: { name: "The Brute", health_mod: 2, hint: "Strong, tough, direct." }
|
||||
2: { name: "The Cunning", health_mod: 0, hint: "Sly, quick, resourceful." }
|
||||
3: { name: "The Guardian", health_mod: 2, hint: "Protective, sturdy, watchful." }
|
||||
4: { name: "The Shadow", health_mod: 0, hint: "Stealthy, elusive, silent." }
|
||||
5: { name: "The Weaver", health_mod: -1, hint: "Arcane, perceptive, strange." }
|
||||
6: { name: "The zealot", health_mod: 1, hint: "Driven, faithful, relentless." }
|
||||
|
||||
max_health:
|
||||
description: "Roll 2d6 + thing modifier. Minimum 2."
|
||||
formula: "2d6 + thing_mod"
|
||||
|
||||
starting_cash:
|
||||
description: "Roll 3d6 × 5 for starting silver."
|
||||
formula: "3d6 * 5"
|
||||
|
||||
failed_career:
|
||||
description: "What you did before adventuring (and failed at)."
|
||||
table:
|
||||
1: "Alchemist's assistant — your master's lab exploded."
|
||||
2: "Barber-surgeon — you lost more patients than you saved."
|
||||
3: "City watch — you were fired for taking bribes / sleeping on duty."
|
||||
4: "Coin counter — you embezzled and fled."
|
||||
5: "Ditch digger — you hit a sewer main and flooded the street."
|
||||
6: "Farmhand — the farm was cursed / burned / eaten by beasts."
|
||||
7: "Grave keeper — the dead started walking, and you walked faster."
|
||||
8: "Hedge knight — your employer died under your 'protection'."
|
||||
9: "Inn keep — the inn was repossessed / burned down."
|
||||
10: "Jester — your jokes landed you in a dungeon."
|
||||
11: "Letter carrier — you lost the mail (all of it, somehow)."
|
||||
12: "Mercenary — your company was routed, you were the sole survivor."
|
||||
|
||||
whats_gone_wrong:
|
||||
description: "Something that's haunting you right now."
|
||||
table:
|
||||
1: "A debt you cannot pay is coming due."
|
||||
2: "A powerful person wants you dead or captured."
|
||||
3: "You're cursed — minor but annoying."
|
||||
4: "You lost something irreplaceable."
|
||||
5: "Someone you love is in danger."
|
||||
6: "You're being blackmailed."
|
||||
7: "You have a secret that would destroy your reputation."
|
||||
8: "An old ally now considers you a betrayer."
|
||||
9: "You're addicted to something dangerous."
|
||||
10: "You witnessed something you were never meant to see."
|
||||
11: "A rival frames you for crimes you didn't commit."
|
||||
12: "Something from your failed career catches up with you."
|
||||
|
||||
chains:
|
||||
description: "Who or what ties you to this world? Roll or choose."
|
||||
table:
|
||||
1: "A sibling who depends on you."
|
||||
2: "An oath you swore and cannot break."
|
||||
3: "A child you're protecting."
|
||||
4: "A mentor who taught you everything."
|
||||
5: "A pet or companion that follows you."
|
||||
6: "A debt of honour to a family."
|
||||
7: "A sacred duty to a forgotten god."
|
||||
8: "A bloodline you're the last of."
|
||||
9: "A promise to a dying friend."
|
||||
10: "A community that raised you."
|
||||
11: "A lover you're searching for."
|
||||
12: "A kingdom you're trying to restore."
|
||||
|
||||
discords:
|
||||
description: "Who or what makes your life harder?"
|
||||
table:
|
||||
1: "A rival who wants what you have."
|
||||
2: "A faction that hunts your kind."
|
||||
3: "An ex-partner full of spite."
|
||||
4: "A religious order that condemns you."
|
||||
5: "A gang you crossed."
|
||||
6: "A noble whose name you sullied."
|
||||
7: "A monster that remembers you."
|
||||
8: "A liar who spreads rumours about you."
|
||||
9: "A creditor who never forgets."
|
||||
10: "A family feud that reignites."
|
||||
11: "A thief who stole your identity."
|
||||
12: "A corrupt official with a grudge."
|
||||
|
||||
gear:
|
||||
description: "Starting equipment. Spend cash on this."
|
||||
table:
|
||||
cheap:
|
||||
- { item: "Rope (30ft)", cost: 1 }
|
||||
- { item: "Torches (3)", cost: 1 }
|
||||
- { item: "Rations (3 days)", cost: 1 }
|
||||
- { item: "Waterskin", cost: 1 }
|
||||
- { item: "Chalk (10 pieces)", cost: 1 }
|
||||
- { item: "Candle", cost: 1 }
|
||||
- { item: "Flint & steel", cost: 1 }
|
||||
- { item: "Small sack", cost: 1 }
|
||||
- { item: "Bedroll", cost: 2 }
|
||||
- { item: "Crowbar", cost: 2 }
|
||||
- { item: "Grappling hook", cost: 3 }
|
||||
- { item: "Lantern", cost: 3 }
|
||||
- { item: "Oil (flask)", cost: 1 }
|
||||
- { item: "Tent", cost: 3 }
|
||||
- { item: "Healing salve (restores 1 HP)", cost: 5 }
|
||||
- { item: "Antitoxin", cost: 5 }
|
||||
luxury:
|
||||
- { item: "Fine clothes", cost: 10 }
|
||||
- { item: "Jewellery", cost: 15 }
|
||||
- { item: "Musical instrument", cost: 10 }
|
||||
- { item: "Writing kit", cost: 8 }
|
||||
- { item: "Spyglass", cost: 20 }
|
||||
|
||||
hittin_tools:
|
||||
description: "Weapons. Each has a damage modifier."
|
||||
table:
|
||||
1: { name: "Dagger", damage_mod: 0, cost: 3, note: "Small, concealable" }
|
||||
2: { name: "Short sword", damage_mod: 0, cost: 5, note: "Reliable" }
|
||||
3: { name: "Long sword", damage_mod: 1, cost: 8, note: "Versatile" }
|
||||
4: { name: "Axe", damage_mod: 1, cost: 7, note: "Can be thrown" }
|
||||
5: { name: "Mace", damage_mod: 1, cost: 6, note: "Bypasses some armour" }
|
||||
6: { name: "Spear", damage_mod: 0, cost: 4, note: "Reach, can be thrown" }
|
||||
7: { name: "Bow", damage_mod: 0, cost: 7, note: "Two-handed, ranged" }
|
||||
8: { name: "Crossbow", damage_mod: 1, cost: 10, note: "Two-handed, slow to reload" }
|
||||
9: { name: "Staff", damage_mod: -1, cost: 1, note: "Two-handed, can be focus" }
|
||||
10: { name: "Flail", damage_mod: 1, cost: 8, note: "Ignores shield bonus" }
|
||||
11: { name: "Hatchet", damage_mod: 0, cost: 4, note: "Light, off-hand" }
|
||||
12: { name: "Improvised", damage_mod: -1, cost: 0, note: "Bottle, chair leg, etc." }
|
||||
|
||||
whack_blunters:
|
||||
description: "Armour reduces incoming damage."
|
||||
table:
|
||||
1: { name: "Padded", reduction: -1, cost: 5, note: "Quiet" }
|
||||
2: { name: "Leather", reduction: -1, cost: 8, note: "Flexible" }
|
||||
3: { name: "Studded leather", reduction: -2, cost: 15, note: "Light" }
|
||||
4: { name: "Chain shirt", reduction: -2, cost: 25, note: "Covers torso" }
|
||||
5: { name: "Scale mail", reduction: -3, cost: 35, note: "Noisy" }
|
||||
6: { name: "Plate", reduction: -4, cost: 50, note: "Heavy, noisy" }
|
||||
7: { name: "Shield", reduction: -1, cost: 10, note: "Can be used to shove" }
|
||||
8: { name: "Helmet", reduction: -1, cost: 8, note: "Only vs head shots" }
|
||||
|
||||
arcane_weavin:
|
||||
description: "Optional. If you play a Weaver, roll to see your arcane flavour."
|
||||
table:
|
||||
1: "Wild magicks — unpredictable, surges happen."
|
||||
2: "Blood magicks — costs health to cast."
|
||||
3: "Bone magicks — requires remains or tokens."
|
||||
4: "Storm magicks — tied to weather and sky."
|
||||
5: "Veil magicks — deals with spirits and the dead."
|
||||
6: "Rune magicks — inscribed, prepared in advance."
|
||||
93
rules/mechanics.md
Normal file
93
rules/mechanics.md
Normal file
@ -0,0 +1,93 @@
|
||||
# The Chaos — Core Mechanics Reference
|
||||
|
||||
## Core Dice
|
||||
|
||||
- **d6**: Standard six-sided die. All rolls use d6.
|
||||
- **Odds (Rapid Rulin')**: Roll 1d6. 4+ favours the character(s). 3- is trouble.
|
||||
- **Traits**: Roll 3d6. If result is **lower than** the relevant trait score, you succeed.
|
||||
|
||||
## Character Stats
|
||||
|
||||
- **Traits** (STR, DEX, WIL): Roll 3d6 for each. Lower is better.
|
||||
- **Max Health**: 2d6 + Thing modifier (min 2).
|
||||
- **Starting Cash**: 3d6 × 5 silver.
|
||||
- **Damage**: 1d6 ± weapon modifier ± armour reduction.
|
||||
|
||||
## Conflict (Combat)
|
||||
|
||||
### Before Conflict (Encounter Start)
|
||||
1. **Distance**: 2d6 × 10 (metres/feet) — how far threat is from character(s).
|
||||
2. **Surprise**: 1d6 — 1-2 characters surprised, 3-4 creatures surprised, 5 both, 6 neither.
|
||||
3. **Grit**: 2d6 — check against grit score or grit tables.
|
||||
|
||||
### During Conflict (Each Round ≈ 10 seconds)
|
||||
1. **State Intentions** — What does each side want to do?
|
||||
2. **Grit** (creatures/NPCs) — Roll 2d6, check grit score/tables.
|
||||
3. **Initiative** — Both sides roll 1d6. Winner acts first. Same = simultaneous.
|
||||
4. **Turns** — Roll 1d6 ± modifiers. 4+ = success. 3- = take a hit.
|
||||
5. **Damage** — 1d6 ± weapon mod ± armour reduction.
|
||||
|
||||
### New Paths (Alternatives to Fighting)
|
||||
- **Run off** — Check conditions and surprise.
|
||||
- **Dodge & Parry** — Roll 1d6 + 1. 4+ move without hit. If lost initiative, enemy gets -1 to hit.
|
||||
- **Parley** — Check grit, use relevant traits. Resolve organically.
|
||||
- **Innovate** — Check complications and grit. Resolve naturally.
|
||||
|
||||
## Exploration Loop
|
||||
|
||||
When exploring perilous areas, time narrows into **6 ten-minute watches** (total = 1 hour).
|
||||
|
||||
Each meaningful action (search, make noise, deal with trap, etc.):
|
||||
|
||||
1. Player states intent
|
||||
2. GM calls for roll (odds or traits)
|
||||
3. Result determines outcome
|
||||
4. Mark a watch box
|
||||
|
||||
After 6 watches, the situation usually changes (random encounter, scene shift, etc.).
|
||||
|
||||
## Wounds & Death
|
||||
|
||||
When reduced to 0 HP, roll 1d6:
|
||||
- **1-2**: Die immediately.
|
||||
- **3-4**: Lasting wound — reduce max health by 1.
|
||||
- **5-6**: -1 modifier on all rolls until healed.
|
||||
|
||||
## Grit Mechanic
|
||||
|
||||
Creatures and NPCs roll 2d6 for grit. Higher = more determined. Check against grit score or:
|
||||
- Result > grit score → they look for the exit.
|
||||
- Result ≤ grit score → they press the attack.
|
||||
|
||||
## Roll Modifiers
|
||||
|
||||
| Situation | Modifier |
|
||||
|-----------|----------|
|
||||
| Favourable position | +1 |
|
||||
| Risky or rushed | -1 |
|
||||
| Desperate | -2 |
|
||||
| Well-prepared | +1 |
|
||||
| Poor visibility | -1 |
|
||||
| Using a relevant trait | +1 |
|
||||
|
||||
## Darkness
|
||||
|
||||
Most creatures see fine in the dark. Characters without a light source:
|
||||
- Roll at -1 on actions
|
||||
- Surprise chance increases
|
||||
- Exploration loop takes longer
|
||||
|
||||
## Complications
|
||||
|
||||
When the dice say "yes, but...":
|
||||
- Success costs something (gear, time, health)
|
||||
- Unexpected noise attracts attention
|
||||
- A secondary problem emerges
|
||||
- The situation escalates
|
||||
|
||||
## Rest & Healing
|
||||
|
||||
- **Short rest** (few hours in relative safety): Recover 1d6 HP.
|
||||
- **Long rest** (full night in safe haven): Recover all HP.
|
||||
- **Healing salve**: Restores 1 HP when applied.
|
||||
- **Antitoxin**: Cures one poisoning.
|
||||
5
run.sh
Executable file
5
run.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
python3 tools/run.py
|
||||
48
session/character.md
Normal file
48
session/character.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Character
|
||||
|
||||
**Name:** Dillion
|
||||
**Thing:** Guardian
|
||||
**Failed Career:** Jester — told a smutty joke about the mayor's daughter, landed in a dungeon.
|
||||
**What's Gone Wrong:** Has a secret: once dressed as a princess and loved it — would destroy his rep.
|
||||
|
||||
## Traits
|
||||
|
||||
- **STR:** 10
|
||||
- **DEX:** 14
|
||||
- **WIL:** 9
|
||||
|
||||
## Vitals
|
||||
|
||||
- **Max Health:** 10
|
||||
- **Current Health:** 10
|
||||
- **Armour:** Leather (-1 reduction)
|
||||
- **Weapon:** Mace (1d6+1)
|
||||
- **Cash:** 37 silver
|
||||
|
||||
## Chains
|
||||
|
||||
Searching for a lover who would love him for his feminine side too.
|
||||
|
||||
## Discords
|
||||
|
||||
Jaggard — a rich merchant whose name Dillion sullied.
|
||||
|
||||
## Gear
|
||||
|
||||
- Mace (1d6+1)
|
||||
- Leather armour (-1)
|
||||
- Rations (9 days)
|
||||
- Torches (6)
|
||||
- Rope (30ft)
|
||||
- Waterskin
|
||||
- Bedroll
|
||||
- Flint & steel
|
||||
- Small sack
|
||||
- Healing salve (restores 1 HP)
|
||||
|
||||
## Notes & Scribbles
|
||||
|
||||
- Mayor's daughter joke → dungeon → exile
|
||||
- Secret princess dress-up
|
||||
- Looking for someone who accepts all of him
|
||||
- Jaggard the merchant holds a grudge
|
||||
17
session/log/2026-06-23.md
Normal file
17
session/log/2026-06-23.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Session Log — 2026-06-23
|
||||
|
||||
## Before Start
|
||||
|
||||
Character created: Dillion the Guardian. Failed jester, secret princess, looking for love, fleeing Jaggard's grudge.
|
||||
|
||||
## What Happened
|
||||
|
||||
-
|
||||
|
||||
## Aftermath
|
||||
|
||||
- **XP / Progression:**
|
||||
- **Loot:**
|
||||
- **World Changes:**
|
||||
- **Next Session Seeds:**
|
||||
- Let's start building the world first.
|
||||
7
session/tweaks.md
Normal file
7
session/tweaks.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Tweaks
|
||||
|
||||
_Any house rules, custom tables, or modifications we've made._
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| | |
|
||||
47
session/world.md
Normal file
47
session/world.md
Normal file
@ -0,0 +1,47 @@
|
||||
# The World
|
||||
|
||||
## The Keep
|
||||
|
||||
_The starting point. A fortified settlement at the edge of The Realm._
|
||||
|
||||
### Notable NPCs
|
||||
|
||||
- _to discover_
|
||||
|
||||
### Services
|
||||
|
||||
- **Inn:** _Splintered Tankard_ — cheap ale, warm stew, gossip
|
||||
- **Smithy:** _to discover_
|
||||
- **Temple:** _to discover_
|
||||
- **Market:** _to discover_
|
||||
- **Guild Hall:** _to discover_
|
||||
|
||||
### Rumours
|
||||
|
||||
| # | Rumour | Status |
|
||||
|---|--------|--------|
|
||||
| | | |
|
||||
|
||||
### Jobs
|
||||
|
||||
| # | Job | Pay | Status |
|
||||
|---|-----|-----|--------|
|
||||
| | | | |
|
||||
|
||||
## The Realm
|
||||
|
||||
_What lies beyond the gates._
|
||||
|
||||
### Locations Discovered
|
||||
|
||||
| Name | Terrain | Threat Level | Notes |
|
||||
|------|---------|-------------|-------|
|
||||
| | | | |
|
||||
|
||||
### Active Threads
|
||||
|
||||
-
|
||||
|
||||
### Factions at Play
|
||||
|
||||
- _to be revealed_
|
||||
97
tools/draw.py
Executable file
97
tools/draw.py
Executable file
@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
draw.py — Draw a card from The Chaos deck.
|
||||
|
||||
Usage:
|
||||
python3 draw.py <deck> <table> [count]
|
||||
|
||||
Decks: souls, cook, creatures, curiosities
|
||||
Tables: see the YAML files (e.g. traits, rumours, type, drink_or_drug)
|
||||
|
||||
Examples:
|
||||
python3 draw.py souls thing
|
||||
python3 draw.py cook rumours 3
|
||||
python3 draw.py creatures type appearance
|
||||
python3 draw.py curiosities creepy_vibe
|
||||
"""
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
import random
|
||||
import os
|
||||
|
||||
DECK_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules', 'deck')
|
||||
|
||||
DECKS = {
|
||||
'souls': 'souls.yaml',
|
||||
'cook': 'cook.yaml',
|
||||
'creatures': 'creatures.yaml',
|
||||
'curiosities': 'curiosities.yaml',
|
||||
}
|
||||
|
||||
def load_deck(name):
|
||||
path = os.path.join(DECK_DIR, DECKS[name])
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def draw_from_table(data, table_name):
|
||||
entry = data.get(table_name)
|
||||
if not entry:
|
||||
print(f"Table '{table_name}' not found in deck.")
|
||||
sys.exit(1)
|
||||
|
||||
table = entry.get('table')
|
||||
if table:
|
||||
keys = sorted(table.keys(), key=int)
|
||||
roll = random.randint(keys[0], keys[-1])
|
||||
result = table[roll]
|
||||
if isinstance(result, dict):
|
||||
return result.get('name', str(result)), roll
|
||||
return result, roll
|
||||
|
||||
one_to_six = entry.get('animal_table') or entry.get('trinket_table')
|
||||
if one_to_six and entry.get('has_table'):
|
||||
r1 = random.randint(1, 6)
|
||||
r2 = random.randint(1, 6)
|
||||
return f"{one_to_six[r1]} and has {entry['has_table'][r2]}", f"{r1},{r2}"
|
||||
|
||||
faction_keys = [k for k in entry if k.startswith('table_')]
|
||||
if faction_keys:
|
||||
key = random.choice(faction_keys)
|
||||
items = entry[key]['factions']
|
||||
return random.choice(items), key
|
||||
|
||||
return None, None
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__.strip())
|
||||
sys.exit(1)
|
||||
|
||||
deck_name = sys.argv[1].lower()
|
||||
table_name = sys.argv[2].lower()
|
||||
count = int(sys.argv[3]) if len(sys.argv) > 3 else 1
|
||||
|
||||
if deck_name not in DECKS:
|
||||
print(f"Unknown deck '{deck_name}'. Choose from: {', '.join(DECKS.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
data = load_deck(deck_name)
|
||||
|
||||
print(f"── [{deck_name}] drawing from '{table_name}' ──")
|
||||
for _ in range(count):
|
||||
result, roll = draw_from_table(data, table_name)
|
||||
if result:
|
||||
if roll is not None:
|
||||
print(f" [{roll}] → {result}")
|
||||
else:
|
||||
print(f" → {result}")
|
||||
else:
|
||||
# Try direct pick from top-level
|
||||
if table_name == 'rules':
|
||||
print(" (No card — consult mechanics.md)")
|
||||
else:
|
||||
print(f" No result for '{table_name}'")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
86
tools/roll.py
Executable file
86
tools/roll.py
Executable file
@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
roll.py — Roll dice for The Chaos TTRPG.
|
||||
|
||||
Usage:
|
||||
python3 roll.py <formula> [modifier]
|
||||
|
||||
Formulas:
|
||||
1d6 Roll 1 six-sided die
|
||||
2d6 Roll 2, sum them
|
||||
3d6 Roll 3, sum them
|
||||
2d6x10 Roll 2d6, multiply by 10
|
||||
3d6*5 Roll 3d6, multiply by 5
|
||||
odds Roll 1d6, show success (4+) or failure (3-)
|
||||
trait N Roll 3d6 vs trait score N — success if under
|
||||
|
||||
Modifier: +/- number added to total
|
||||
|
||||
Examples:
|
||||
python3 roll.py 1d6
|
||||
python3 roll.py 3d6
|
||||
python3 roll.py 2d6x10
|
||||
python3 roll.py odds
|
||||
python3 roll.py trait 9
|
||||
python3 roll.py 1d6 +1
|
||||
"""
|
||||
|
||||
import sys
|
||||
import random
|
||||
import re
|
||||
|
||||
def roll_dice(count, sides=6):
|
||||
return [random.randint(1, sides) for _ in range(count)]
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__.strip())
|
||||
sys.exit(1)
|
||||
|
||||
formula = sys.argv[1].lower()
|
||||
modifier = 0
|
||||
if len(sys.argv) >= 3:
|
||||
try:
|
||||
modifier = int(sys.argv[2].replace('+', ''))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if formula == 'odds':
|
||||
r = random.randint(1, 6)
|
||||
outcome = 'SUCCESS (favours character)' if r >= 4 else 'FAILURE (favours trouble)'
|
||||
print(f" [{r}] → {outcome}")
|
||||
return
|
||||
|
||||
if formula == 'trait' and len(sys.argv) >= 3:
|
||||
target = int(sys.argv[2])
|
||||
rolls = roll_dice(3)
|
||||
total = sum(rolls)
|
||||
outcome = 'SUCCESS' if total < target else 'FAILURE'
|
||||
print(f" [{', '.join(map(str, rolls))}] = {total} vs trait {target} → {outcome}")
|
||||
return
|
||||
|
||||
m = re.match(r'(\d+)d6(?:\s*([xX*])\s*(\d+))?', formula)
|
||||
if not m:
|
||||
print(f"Unknown formula: {formula}")
|
||||
sys.exit(1)
|
||||
|
||||
count = int(m.group(1))
|
||||
rolls = roll_dice(count)
|
||||
total = sum(rolls) + modifier
|
||||
|
||||
mult = m.group(2)
|
||||
mult_val = int(m.group(3)) if m.group(3) else 1
|
||||
|
||||
if mult:
|
||||
total = total * mult_val
|
||||
|
||||
label = f"×{mult_val}" if mult else ""
|
||||
mod_str = f" {'+' if modifier >= 0 else ''}{modifier}" if modifier != 0 else ""
|
||||
|
||||
if count > 1:
|
||||
print(f" [{', '.join(map(str, rolls))}] = {sum(rolls)}{mod_str}{label} → {total}")
|
||||
else:
|
||||
print(f" [{rolls[0]}]{mod_str}{label} → {total}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
241
tools/run.py
Executable file
241
tools/run.py
Executable file
@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
run.py — The Chaos TTRPG Session Client
|
||||
|
||||
Layout: banner top | log (main) + character (right) | input bottom.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import Input, Static
|
||||
|
||||
|
||||
# ── Paths ────────────────────────────────────────────────
|
||||
BASE = Path(__file__).resolve().parent.parent
|
||||
SESSION = BASE / 'session'
|
||||
LOG_DIR = SESSION / 'log'
|
||||
CHAR_PATH = SESSION / 'character.md'
|
||||
WORLD_PATH = SESSION / 'world.md'
|
||||
TODAY = date.today().isoformat()
|
||||
LOG_PATH = LOG_DIR / f'{TODAY}.md'
|
||||
|
||||
REFRESH_SECS = 2
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────
|
||||
def ensure_log():
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if not LOG_PATH.exists():
|
||||
LOG_PATH.write_text(f"# Session Log — {TODAY}\n\n")
|
||||
|
||||
def append_log(text):
|
||||
with open(LOG_PATH, 'a') as f:
|
||||
f.write(f"- {text}\n")
|
||||
|
||||
def read_log_tail(n=200):
|
||||
if not LOG_PATH.exists():
|
||||
return []
|
||||
lines = LOG_PATH.read_text().splitlines()
|
||||
return [l for l in lines if l.strip() and not l.startswith('#')][-n:]
|
||||
|
||||
def read_char_sheet():
|
||||
if not CHAR_PATH.exists():
|
||||
return ["—— No character yet ——"]
|
||||
lines = CHAR_PATH.read_text().splitlines()
|
||||
out = []
|
||||
for l in lines:
|
||||
s = l.rstrip()
|
||||
if s.startswith('**') and ':' in s:
|
||||
out.append(s.strip('*').strip())
|
||||
elif s.startswith('- **'):
|
||||
out.append(s.lstrip('- ').strip('*').strip())
|
||||
return out or ["—— No character yet ——"]
|
||||
|
||||
|
||||
# ── Status summary ───────────────────────────────────────
|
||||
def status_summary():
|
||||
if not CHAR_PATH.exists():
|
||||
return "no character"
|
||||
lines = CHAR_PATH.read_text().splitlines()
|
||||
name = "?"
|
||||
health = "?"
|
||||
for l in lines:
|
||||
if l.startswith('**Name:**'):
|
||||
name = l.split(':', 1)[1].strip().strip('_').strip('*')
|
||||
if l.startswith('**Current Health:**'):
|
||||
h = l.split(':', 1)[1].strip().strip('_').strip('*')
|
||||
if h:
|
||||
health = h
|
||||
if l.startswith('**Max Health:**'):
|
||||
m = l.split(':', 1)[1].strip().strip('_').strip('*')
|
||||
if m and health == '?':
|
||||
health = m
|
||||
return f"{name} ❤ {health}"
|
||||
|
||||
|
||||
# ── Log line count ───────────────────────────────────────
|
||||
def log_count():
|
||||
return len(read_log_tail())
|
||||
|
||||
|
||||
# ── Auto-refreshing panels ───────────────────────────────
|
||||
class AutoStatic(Static):
|
||||
"""A Static that reloads its content on an interval."""
|
||||
|
||||
def load(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def on_mount(self):
|
||||
self.load()
|
||||
self.set_interval(REFRESH_SECS, self.load)
|
||||
|
||||
|
||||
class TranscriptPane(AutoStatic):
|
||||
def load(self):
|
||||
lines = read_log_tail()
|
||||
self.update("\n".join(lines[-80:]))
|
||||
|
||||
|
||||
class CharPane(AutoStatic):
|
||||
def load(self):
|
||||
lines = read_char_sheet()
|
||||
self.update("\n".join(f" {l}" for l in lines))
|
||||
|
||||
|
||||
class StatusBar(AutoStatic):
|
||||
def load(self):
|
||||
char = status_summary()
|
||||
count = log_count()
|
||||
self.update(f"{char} │ {count} entries │ {TODAY}")
|
||||
|
||||
|
||||
# ── The App ──────────────────────────────────────────────
|
||||
class ChaosTUI(App):
|
||||
TITLE = "The Chaos"
|
||||
CSS = """
|
||||
Screen {
|
||||
background: #121212;
|
||||
}
|
||||
|
||||
/* ── Top banner ── */
|
||||
#banner {
|
||||
dock: top;
|
||||
height: 1;
|
||||
background: #2a2a2a;
|
||||
color: #e0ad4c;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Bottom input ── */
|
||||
#input-row {
|
||||
dock: bottom;
|
||||
height: 3;
|
||||
background: #252525;
|
||||
padding: 0 0;
|
||||
border-top: solid #3a3a3a;
|
||||
}
|
||||
Input {
|
||||
background: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
border: none;
|
||||
margin: 1 1;
|
||||
}
|
||||
Input:focus {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* ── Main area: log (left) + sidebar (right) ── */
|
||||
#main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Log column */
|
||||
#log-col {
|
||||
border-right: solid #3a3a3a;
|
||||
background: #111111;
|
||||
}
|
||||
#log-header {
|
||||
background: #1d2d1d;
|
||||
color: #7dcd7d;
|
||||
text-style: bold;
|
||||
padding: 0 1;
|
||||
height: 1;
|
||||
}
|
||||
#transcript {
|
||||
padding: 0 1;
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
#sidebar {
|
||||
width: 36;
|
||||
min-width: 28;
|
||||
background: #181818;
|
||||
}
|
||||
#side-header {
|
||||
background: #2d2d3a;
|
||||
color: #b0a0e0;
|
||||
text-style: bold;
|
||||
padding: 0 1;
|
||||
height: 1;
|
||||
}
|
||||
#char-content {
|
||||
padding: 0 1;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
#status-bar {
|
||||
background: #222222;
|
||||
color: #888888;
|
||||
padding: 0 1;
|
||||
height: 1;
|
||||
text-style: italic;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("ctrl+c", "quit", "Quit"),
|
||||
("escape", "quit", "Quit"),
|
||||
]
|
||||
|
||||
def compose(self):
|
||||
yield Static(f"⚔ The Chaos ╎ {TODAY}", id="banner")
|
||||
with Horizontal(id="main"):
|
||||
with Vertical(id="log-col"):
|
||||
yield Static("LOG", id="log-header")
|
||||
yield TranscriptPane(id="transcript")
|
||||
with Vertical(id="sidebar"):
|
||||
yield Static("CHARACTER", id="side-header")
|
||||
yield CharPane(id="char-content")
|
||||
yield StatusBar(id="status-bar")
|
||||
with Horizontal(id="input-row"):
|
||||
self.input = Input(placeholder=" What do you do? (just type)", id="input")
|
||||
yield self.input
|
||||
|
||||
def on_mount(self):
|
||||
ensure_log()
|
||||
self.input.focus()
|
||||
|
||||
@on(Input.Submitted, "#input")
|
||||
def on_input(self, event: Input.Submitted):
|
||||
text = event.value.strip()
|
||||
if text:
|
||||
append_log(text)
|
||||
self.input.clear()
|
||||
self.query_one(TranscriptPane).load()
|
||||
self.query_one(StatusBar).load()
|
||||
|
||||
|
||||
def main():
|
||||
app = ChaosTUI()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user