A dice based dungeon game
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-03 10:41:18 +02:00
simulator Updating simulator 2026-09-03 10:41:18 +02:00
.gitignore Adding simulator 2026-08-28 13:02:00 +02:00
GameRules.md Fixes 2026-08-29 18:53:48 +02:00
PaperPrototype_v1.md Balancing 2026-08-29 10:00:13 +02:00
PaperPrototype_v2.md Updating simulator 2026-09-03 10:41:18 +02:00
README.md Updating simulator 2026-09-03 10:41:18 +02:00

Dice Dungeon game (Working Title)

This repository contains various files for my dice-based single player dungeon game. These include:

  • Game rules
  • Card definitions
  • Code projects (e.g. simulator)
  • Graphics
  • etc.

Simulator

A headless Python simulator of the combat rules. It plays a deliberately unsmart bot (activates whatever its dice allow, re-rolls unused dice up to twice) against a monster and reports statistics, used for balancing.

Requirements: Python 3.10+, standard library only.

Running the simulator

From the repository root:

# One battle, every step printed to the console (good for debugging):
./simulator/start_simulator.py -n 1 -v

# One battle, full details written to a file (overwrites it), stats on console:
./simulator/start_simulator.py -n 1 --log-file battle.log

# A big statistics-only run (no detailed logging, fast):
./simulator/start_simulator.py -n 1000000 --seed 42

(Alternatively cd simulator first, then run ./start_simulator.py ....)

-n 10000 (10,000 battles, stats only) is the default when no options are given.

Options:

Option Description
-n, --battles Number of battles to simulate, must be >= 1 (default: 10000)
--log-file FILE Write the full details of every battle to FILE (file is overwritten)
-v, --verbose Also print the full battle details to the console
--seed N Random seed for reproducible runs

A run of 1,000,000 battles takes roughly 12-14 minutes without logging. Enabling --log-file or -v slows the run down and, for large battle counts, produces a very large file.

Running the tests

From the repository root:

cd simulator && python3 -m unittest discover -s tests

Lower-level CLI

The simulator module can also be run directly:

cd simulator && python3 -m dice_sim.simulate --battles 1000 --seed 42 --debug --verbose-battles 2

--debug prints detailed combat logs to the console; --verbose-battles N limits detailed logging to the first N battles.

How it is organised

simulator/
├── start_simulator.py     # easy-to-use launcher (battle count, log file, stats)
├── dice_sim/
│   ├── dice.py            # dice (D4/D6/D8/D10/D12) and rolling
│   ├── entities.py        # Player and Monster
│   ├── state.py           # GameState: dice, threat, stored dice, cards, outcome
│   ├── cards/             # one self-contained module per card
│   │   ├── base.py        # Card/Action protocol (no card logic)
│   │   ├── conditions.py  # small shared dice-condition helpers
│   │   ├── sword.py       # Sword: Attack, Super Attack, Parade
│   │   ├── shield.py      # Shield: Block, Ram, Brace for impact
│   │   ├── health_potion.py  # Health Potion: Drink, Inject (consumable)
│   │   ├── crossbow.py    # Crossbow: Basic Shot, Heavy Shot, Sniper
│   │   ├── boots.py       # Boots: Evade, Charge
│   │   └── backpack.py    # Backpack: Store, Rummage
│   ├── engine.py          # the battle loop (roll -> play -> damage -> cleanup)
│   ├── ai.py              # the simple bot
│   └── simulate.py        # statistics, default battle setup, low-level CLI
└── tests/                 # unittest suite

Each card is self-contained in its own module under dice_sim/cards/: the dice checks and the effects live next to the card, so new cards are added as new files. conditions.py only holds the small helpers that several cards share. The engine never hardcodes card logic: it calls the turn strategy (ai.py) and the actions.

Adding a new card

Each card is one self-contained file in dice_sim/cards/. To add a card:

  1. Create the card file, e.g. dice_sim/cards/dagger.py. Every action of the card is a small Action subclass whose can_activate checks the assigned dice (and, if needed, the game state) and whose execute implements the effect. Checks and effects live in this file, not in a central place:

    # dice_sim/cards/dagger.py
    """The Dagger: a short, quick blade."""
    
    from __future__ import annotations
    
    from typing import TYPE_CHECKING, List
    
    from .base import Action, Card
    from .conditions import is_odd
    
    if TYPE_CHECKING:
        from ..state import GameState
    
    
    class DaggerThrust(Action):
        """Deal 3 damage to the monster with a single odd die."""
    
        def __init__(self) -> None:
            super().__init__("Thrust")
    
         def can_activate(self, assigned_dice: List[int], indices: List[int], state: GameState) -> bool:
             return len(assigned_dice) == 1 and is_odd(assigned_dice)
    
         def execute(self, assigned_dice: List[int], indices: List[int], state: GameState) -> None:
             state.monster.damage(3)
    
    
    class Dagger(Card):
        def __init__(self) -> None:
            super().__init__(
                "Dagger",
                "weapon",
                [DaggerThrust()],
                "A short, quick blade.",
                "normal",
            )
    
  2. Export it in dice_sim/cards/__init__.py: add from .dagger import Dagger and "Dagger" to __all__. (Optionally do the same in dice_sim/__init__.py if it should be part of the public API.)

  3. Use it: pass it to a player, e.g. Player("Hero", 30, [Sword(), Dagger()], [D6, D6, D6]), or add it to default_player_cards() in dice_sim/cards/__init__.py if it belongs to the standard loadout used for balancing.

  4. Add tests in tests/test_cards.py following the existing pattern: check can_activate for a few dice combinations and verify the effect of execute on a GameState.

  5. Run the suite (cd simulator && python3 -m unittest discover -s tests) and, if the card joins the standard loadout, re-run a large simulation to check the effect on the win rate (see Balance tuning below).

Notes:

  • A card may offer several actions: define one Action subclass per action and list them all in the card's constructor (see sword.py).
  • Consumable cards mark their action with is_consumable=True (see health_potion.py); the engine then discards the card after the round.
  • can_activate and execute receive the assigned dice values and their indices in the player's dice, so actions can manipulate the dice pool (e.g. Backpack: Store banks the dice at the given indices).
  • can_activate also receives the game state, so actions can depend on more than the dice (e.g. "only usable while the player is below 5 HP": return state.player.hp <= 5).
  • The bot uses can_activate_in_hand(hand, state) as a cheap filter before enumerating dice combinations; the default implementation searches all subsets of up to 4 dice, but cards with simple pattern checks (e.g. "any even die") should override it with a direct check.
  • execute receives the game state too, so actions can do anything the state allows: damage or heal, remove monster threat dice, manipulate the player's dice, and so on.
  • For dice checks that several cards share (doubles, straights, sums, ...), reuse or extend the helpers in dice_sim/cards/conditions.py.

Balance tuning

The default battle is defined in make_default_battle in dice_sim/simulate.py: a Dragon with 50 HP and attack dice [D6, D6, D12] (avg ~13.5 threat per round) vs a Hero with 25 HP, dice [D6, D6, D6, D6, D12, D12] and the standard cards (Sword, Shield, Health Potion, Crossbow, Boots, Backpack).

The target is a human win rate, not the bot's. The v2 card set rewards multi-turn planning (storing dice in the Backpack, Rummage for a needed face, Charge into an attack, setting up Heavy Shots) that the deliberately unsmart bot cannot do: with the current numbers it wins only about 7% of battles, while the monster's threat is high enough that a human needs to block or evade almost every round just to survive. (Each action is usable at most once per round, so a Shield Block cannot be stacked several times in one turn.) Change the HP, dice or card list there and re-run a large simulation to check the effect.