diff --git a/deepfall/RULEBOOK.md b/deepfall/RULEBOOK.md new file mode 100644 index 0000000..1e16b5b --- /dev/null +++ b/deepfall/RULEBOOK.md @@ -0,0 +1,290 @@ +# DEEPFALL — Rulebook + +*Reikhelm, the deep hold, is sinking into the Maw. Delve fast, hold ground, and know +when to die. The deepest grave is the brightest glory.* + +**Players:** 2–6 · **Time:** 45–75 min · **Ages:** 12+ + +> This is the playtest rulebook. Every number here is grounded in the DEEPFALL balance +> simulator (`deepfall-sim`); see `FINDINGS.md` for what the data said and `DESIGN.md` +> for the deeper why. Where the sim used continuous coefficients, this rulebook rounds +> them to clean tabletop integers. + +--- + +## 1. The idea & how you win + +You lead a **delve-company** plundering a collapsing megadungeon. Beneath the deepest +floor waits **the Maw**, a rising hunger that devours the hold one floor at a time, **from +the bottom up** — so the richest, deepest rooms are doomed *first*. + +You don't win by surviving. You win by **Glory**, and the surest fortunes come from +spending a company deliberately: send a crew quietly **to ground** with its loot, or hurl +it into a **Worthy End** at the bottom and let the Maw take it gloriously. Nobody is ever +eliminated — a dead crew becomes Glory and silent territory, and you muster a fresh one. + +**The game ends when the Maw eats Floor 1. Most Glory wins.** + +--- + +## 2. Components + +| Component | Count | Notes | +|---|---|---| +| **Room tiles** | ~48 deck | 10 themes (Threshold → Throne); each shows value, ⚔ threat, 🜏 lock, produce icon | +| **Floor strips** (or trays) | 6 | The dungeon, top (Floor 1) to bottom (Floor 6) | +| **The Maw marker** + **Dread track** | 1 | The doom clock | +| **Delve dice** | 7 types | Recruit / Warrior / Scholar / Priest / Rogue / Champion / Relic (see §5) | +| **Dice bags** | 1 per player | Your bag is your engine | +| **Lineage cards** | 6 | Your "race" half | +| **Calling cards** | 6 | Your "class/faction" half | +| **Player mats** | 1 per player | Party, held rooms, Mettle, hand | +| **Control markers** + **Fallen (skull) markers** | per player | Claim rooms / mark your dead | +| **Mettle & Loot tokens**, **Glory track**, **Denizen rep tracks** | shared | The economy | + +The six dice **faces**: ⚔ **Steel** · 🜏 **Cunning** · ✚ **Faith** · 💰 **Greed** · ✦ **Mettle** · ☠ **Dread**. + +--- + +## 3. Setup + +1. **Build the dungeon.** Stock the six floor strips, **rooms-per-floor = number of + players** (so a 4-player game has 4 rooms per floor, a 6-player game has 6). Draw room + tiles weighted richer with depth: Floor 1 is mostly Threshold/Stone; Floor 6 holds the + Throne and the Vaults. *(Easiest: use the **reikhelm Generator Companion** — give it a + seed and it prints the exact layout. Every game, a new dungeon.)* +2. **Depth track.** Each floor has a **Depth Value** printed beside it: + + | Floor | 1 | 2 | 3 | 4 | 5 | 6 | + |---|---|---|---|---|---|---| + | **Depth Value** | ×1 | ×2 | ×2 | ×3 | ×3 | ×4 | + + A room's **Glory at collapse = its tile value × its floor's Depth Value.** (A Vault, + value 6, on Floor 6 is worth `6 × 4 = 24`.) +3. **The Maw** starts just below Floor 6. Set the shared **Dread track** to 0; its length + is **3 + number of players** (see §9). +4. **Draft your company.** Each player takes one **Lineage** + one **Calling** (deal 2 of + each, keep 1 of each, or draft) and builds their **starting dice bag** as printed on the + Lineage card. +5. Give each player their markers, a player mat, and 0 Glory. Seed the shared **Recruit + Pool** with dice of each recruitable type. Pick a first player; pass the marker + clockwise each round. + +--- + +## 4. The company: Lineage × Calling + +Two halves smash into one identity. Each grants an **advantage** and a **cost** — no free +builds. + +### Lineages (the "race" — sets your starting bag + a passive) + +| Lineage | Advantage | Cost | +|---|---|---| +| **Dvergar** | Ignore 1 point of a room's ⚔ threat when claiming | −1 ✚ Faith (courts Dread) | +| **Wisp** | Draw +1 die every hand | Bigger hand rolls more ☠ Dread | +| **Revenant** | Convert leftover ☠ Dread into ✦ Mettle (peril as fuel) | Cannot cancel Dread with Faith | +| **Gnoll** | +Loot when you claim a **combat** room (Den/Forge/Crypt/Throne) | −1 ✚ Faith | +| **Sylphid** | +1 Move and +1 🜏 Cunning | Fragile: a Setback sheds an extra die | +| **Golem-born** | Never lose dice to a Setback | −1 hand size (rigid) | + +### Callings (the "class/faction" — special + recruit pool) + +| Calling | Special | Recruits | +|---|---|---| +| **Reaver** | Pillage: gain Loot every time you claim a room | Warrior (cheap) | +| **Lorekeeper** | Open **Vaults solo** — no trinity needed | Scholar | +| **Wardancer** | Free **Last Stand**; Worthy End **×2.2**; +1 marker strength; +1 Move | Champion | +| **Hierophant** | Cancel +1 Dread; heal Setbacks; +25% held-room production | Priest | +| **Warden** | Your control markers count **double** for majority | Champion | +| **Prospector** | Cheaper recruits; +1 hand; can find **Relic** dice | Relic | + +6 × 6 = **36 companies.** They are deliberately *spicy, not flat* — Dvergar and Reaver run +a touch strong, Wardancer a touch swingy — but the sim confirms **every combo is viable** +(no dead picks). + +--- + +## 5. The dice + +Your bag holds delve dice. Each is a d6; the types differ only in their face mix: + +| Die (color) | ⚔ | 🜏 | ✚ | 💰 | ✦ | ☠ | +|---|---|---|---|---|---|---| +| **Recruit** (grey) | 1 | 1 | 1 | 1 | 1 | 1 | +| **Warrior** (red) | 3 | – | – | 1 | 1 | 1 | +| **Scholar** (blue) | – | 2 | 1 | 1 | 1 | 1 | +| **Priest** (white) | – | 1 | 2 | 1 | 1 | 1 | +| **Rogue** (green) | 1 | 1 | – | 2 | 1 | 1 | +| **Champion** (gold) | 2 | 1 | 1 | 1 | 1 | – | +| **Relic** (purple) | – | – | 1 | 3 | 2 | – | + +--- + +## 6. A round + +A round has two phases. + +### Phase A — The Delve (players take turns, clockwise) + +> *Speed tip:* roll your opening hand during the previous player's turn, so your turn is +> decisions, not dice-fishing. With 5–6 players this keeps downtime low. + +On your turn: + +1. **Draw & roll** a hand from your bag (default **4 dice**, ± Lineage/Calling). +2. **Press On?** *(push-your-luck)* You may draw & roll **another** hand for more symbols + — as many times as you dare, stopping whenever you like. **But every Press On feeds the + Maw** (add a Dread to the shared track), and rolling too much ☠ risks a **Setback**. +3. **Resolve Dread.** Cancel ☠ with ✚ Faith one-for-one (Hierophant cancels one extra; + Revenant converts instead — see its card). If **3 or more ☠ remain**, suffer a + **Setback**: return your lowest die to the box (Golem-born ignores this; Sylphid loses + an extra; Hierophant heals one). +4. **Spend your symbols** in any order: + - **Move** between floors. You get **1 Move** free (± Lineage/Calling); buy extra Moves + with 🜏 Cunning or ✦ Mettle (1 each). + - **Claim a room** on your current floor (see §7). + - **Loot:** each 💰 Greed banks Loot, worth more the deeper you are. + - **Recruit:** spend ✦ Mettle to add a die of your Calling's type to your bag (§8). +5. **Choose your exit** (see §7.3): **Stay**, **Cash Out**, or **declare a Last Stand**. + +### Phase B — The Maw (everyone, together — the big moment) + +1. **Advance the Maw.** The Maw rises **1 floor** automatically each round, **+1 more** if + the shared **Dread track** is full (then reset it). *(It never eats more than one floor + per round — see §9 — so "one floor away" is always an honest warning.)* +2. **Collapse** the floor it reaches: + - **Score it.** For each room, whoever has the most marker strength takes its Glory + (`tile value × Depth Value`). Ties split, rounding down. **Warden** markers count + double; **Fallen** markers count as 1. + - **Value rises.** Half the value of every **unclaimed** room **rises to the floor + above** as refugee plunder (add a plunder token). *This is why the top floors swell + into the richest finale — never abandon the shallows.* + - **The Maw takes the floor**, and anyone still on it (see §7.3). +3. Pass the first-player marker. New round. + +The game ends the instant **Floor 1 collapses.** + +--- + +## 7. The heart of the game + +### 7.1 Claiming rooms (area control + the trinity) + +To **claim** a room you must, across this turn, produce: +- **⚔ Steel ≥ the room's threat** (fight through the monsters), **and** +- **🜏 Cunning ≥ the room's lock** (crack the mechanism). + +**Vaults** also demand the **trinity**: you must have rolled at least one ⚔, one 🜏, *and* +one ✚ this turn (the EverQuest holy trinity, made literal) — unless you're a **Lorekeeper**. + +Place a **control marker**; its **strength = 1 + extra symbols you poured in** (Warden ×2). +Didn't quite make it? Your Steel/Cunning **carry over as assault progress** — keep chipping +at the same room next turn. A near-miss is never a total loss. + +Holding a room makes it **produce** each turn (a Forge throws Steel, a Library Cunning, a +Vault Greed) — your engine grows on the ground you keep, right up until the Maw takes it. + +### 7.2 Room tiles at a glance + +| Theme | Value | ⚔ Threat | 🜏 Lock | Produces | Note | +|---|---|---|---|---|---| +| Threshold | 1 | 0 | 0 | – | safe mouth | +| Stone | 2 | 1 | 0 | – | plain | +| Cistern | 3 | 1 | 1 | ✦ | flooded | +| Den | 3 | 3 | 0 | ⚔ | combat lair | +| Crypt | 3 | 2 | 1 | ✚ | undead (combat) | +| Hall | 4 | 1 | 1 | ✦ | pillared | +| Library | 4 | 1 | 2 | 🜏 | loot-rich, locked | +| Forge | 5 | 4 | 1 | ⚔ | lava (combat) | +| **Vault** | 6 | 2 | 2 | 💰 | **needs trinity** | +| **Throne** | 7 | 5 | 2 | 💰 | boss (combat), deepest | + +### 7.3 The three exits (this is the game) + +Every turn, every crew faces the same choice. This triangle *is* DEEPFALL: + +- **STAY / Press On** — keep the crew active: keep recruiting, holding, producing. More + upside, but the Maw is climbing, and a crew caught flat-footed loses **everything + unbanked and all its active territory**. +- **CASH OUT ("Go to Ground")** — retire the crew. **Bank** its Loot + a **depth dividend** + (deepest floor reached × 2). Its markers become permanent **Fallen** markers (strength 1) + that still score at every future collapse. Then muster a **fresh** company at the + frontier. Safe, repeatable, keeps your dead earning. *(Decline, from Small World.)* +- **WORTHY END ("Last Stand")** — declare it (free if you hold a Throne/Vault, or as a + **Wardancer**). When the Maw **Takes** your active crew, score a huge **Worthy End**: + `deepest floor × 2.5` (Wardancer ×2.2 on top), and **the Maw consumes your held rooms + clean** so no rival inherits them. The single biggest payout — and all-or-nothing. + *(Valhalla, from Blood Rage.)* + +Being Taken **without** a Last Stand is the game's one real disaster (lose loot + territory +for nothing). The exits above are *always* offered, so a careful player never falls into +it — and the simulator confirms it almost never happens. + +> **Design note:** under perfectly greedy play the bots treat Cash Out and Worthy End as +> substitutes, so aggression slightly over-wins. At a human table the *denial* (salting a +> rival's inheritance) and the sheer drama of a deep martyr make Worthy End pull its +> weight. This is the one spot still under playtest — see `FINDINGS.md`. + +--- + +## 8. Recruiting (building your bag) + +Spend ✦ **Mettle** (default cost 2) to take a die from the Recruit Pool into your bag — +the type set by your **Calling** (Reaver→Warrior, Lorekeeper→Scholar, etc.; Prospector +gets the rare **Relic**). A bigger, sharper bag means stronger hands *and* more Press-On +fuel. This is your in-game engine (Quarriors' dice-building). + +--- + +## 9. The Maw & player count + +The Maw rises **1 floor per round**, **+1** whenever the shared **Dread track** fills (then +it resets). Players add Dread to the track every time they **Press On** and whenever +uncanceled ☠ is rolled — so the table's collective greed literally pulls the doom down +faster. **The Maw never collapses more than one floor in a single round.** + +**Scaling (validated to 11-round games at every count):** + +| Players | Rooms / floor | Dread track length | +|---|---|---| +| 2 | 4 | 5 | +| 3 | 4 | 6 | +| 4 | 4 | 7 | +| 5 | 5 | 8 | +| 6 | 6 | 9 | + +Rooms-per-floor scaling keeps contention (and the fight for majorities) constant; the +longer Dread track on a bigger table absorbs the extra collective Dread so the game still +ends in ~11 rounds with tight margins and a near-zero feel-bad rate. + +--- + +## 10. Turn quick-reference + +``` +YOUR TURN (Delve Phase) + 1. Roll hand (4 dice ± mods) + 2. Press On? → +1 Dread to track, risk a Setback (repeat / stop) + 3. Dread: cancel with Faith; 3+ left = Setback (lose a die) + 4. Spend: Move · Claim a room · bank Loot (Greed) · Recruit (Mettle) + 5. Exit: STAY / CASH OUT (bank + Fallen markers) / LAST STAND + +MAW PHASE (all together) + • Maw rises 1 floor (+1 if Dread track full → reset) + • Collapse: score majorities (value × depth) → unclaimed value RISES up + → Take anyone still on the floor (Worthy Ends fire here) + • Floor 1 collapses → GAME OVER, most Glory wins +``` + +--- + +## 11. The generator companion (procgen) + +DEEPFALL's dungeon is a **real reikhelm dungeon**. The companion turns a seed into either: +- a **Setup Sheet** ("Floor 6: Vault, Throne, Forge, Crypt…") you copy with the tile deck — + 30-second setup, infinite dungeons from one box; or +- a fully **AI-rendered board** for that seed (the deluxe one-off / campaign mode). + +Same generator that powers the reikhelm playground — so the board, the variety, and the +art all come from the engine you already have. diff --git a/deepfall/deepfall-sim/src/main.rs b/deepfall/deepfall-sim/src/main.rs index 7a83296..e3cfd3a 100644 --- a/deepfall/deepfall-sim/src/main.rs +++ b/deepfall/deepfall-sim/src/main.rs @@ -32,6 +32,15 @@ fn main() { None => Config::default(), }; + // `--players N` runs a quick player-count scaling diagnostic instead of the full + // sweeps (used to ground the rulebook's 6-player rules in data). + if let Some(n) = arg_value(&args, "--players").and_then(|s| s.parse::().ok()) { + for pc in [4usize, 5, 6, n].iter().copied().collect::>() { + diagnose_players(&cfg, seeds, pc); + } + return; + } + fs::create_dir_all(&out).expect("create out dir"); println!("DEEPFALL sweeps seeds={seeds} out={out} config={}", cfg_path.as_deref().unwrap_or("")); println!("{}", "=".repeat(72)); @@ -203,6 +212,69 @@ fn strategy_sweep(cfg: &Config, seeds: u64, out: &str) -> StratOut { } } +// --------------------------------------------------------------------------------- +// Player-count scaling diagnostic +// --------------------------------------------------------------------------------- + +fn diagnose_players(base: &Config, seeds: u64, players: usize) { + // Linear board scaling: rooms-per-floor = player count (keeps rooms-per-capita and + // thus contention roughly constant). Maw dread-push is scaled by 4/players so the + // larger table's extra collective Dread doesn't crash the game length. + let mut cfg = base.clone(); + cfg.rooms_per_floor = players as u32; + cfg.maw_dread_push = base.maw_dread_push * 4.0 / players as f64; + + let mut sum_rounds = 0u64; + let (mut rmin, mut rmax) = (u32::MAX, 0u32); + let mut sum_margin = 0.0; + let mut seat_wins = vec![0u64; players]; + let mut feelbad = 0u64; + let mut worthy = 0u64; + let mut cashout = 0u64; + let mut games = 0u64; + + for seed in 0..seeds { + let roster: Vec<(Strategy, Option<(Lineage, Calling)>)> = + (0..players).map(|i| (ALL_STRATEGIES[i % 4], None)).collect(); + let r = Game::new(cfg.clone(), seed.wrapping_mul(131), &roster).run(); + games += 1; + sum_rounds += r.rounds as u64; + rmin = rmin.min(r.rounds); + rmax = rmax.max(r.rounds); + let wg = r.players[r.winner].glory; + sum_margin += if wg > 0.0 { r.margin / wg } else { 0.0 }; + seat_wins[r.winner] += 1; + feelbad += r.players.iter().map(|p| p.takes_no_laststand as u64).sum::(); + worthy += r.players.iter().map(|p| p.worthy_ends as u64).sum::(); + cashout += r.players.iter().map(|p| p.cashouts as u64).sum::(); + } + + let g = games as f64; + println!( + "\n{players}P (rooms/floor={}, dread_push={:.3})", + cfg.rooms_per_floor, cfg.maw_dread_push + ); + println!( + " length: mean {:.1} rounds ({rmin}-{rmax}) margin: {:.1}% feelbad: {:.3}/game", + sum_rounds as f64 / g, + sum_margin / g * 100.0, + feelbad as f64 / g + ); + let seat_pct: Vec = seat_wins + .iter() + .enumerate() + .map(|(i, w)| format!("P{i}:{:.0}%", *w as f64 / g * 100.0)) + .collect(); + let fair = 100.0 / players as f64; + println!(" seat win% (fair={fair:.0}%): {}", seat_pct.join(" ")); + println!( + " exits/game: cashout {:.1}, worthy {:.2}, feelbad {:.2}", + cashout as f64 / g, + worthy as f64 / g, + feelbad as f64 / g + ); +} + // --------------------------------------------------------------------------------- // Sweep 2: combo asymmetry // ---------------------------------------------------------------------------------