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:
Dejvino 2026-06-23 23:15:17 +02:00
commit d4a19ef438
15 changed files with 1382 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.pyc
.env

65
AGENTS.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
python3 tools/run.py

48
session/character.md Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
# Tweaks
_Any house rules, custom tables, or modifications we've made._
| Date | Change |
|------|--------|
| | |

47
session/world.md Normal file
View 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
View 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
View 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
View 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()