Skip to main content

Wyrdbound Dice

A comprehensive dice rolling library for tabletop RPGs, designed to handle complex dice expressions with mathematical precision and extensive system support.

This library is designed for use in wyrdbound, a text-based RPG system that emphasizes narrative and player choice.

CI Python 3.8+ License: MIT Code style: black

📣 This library is experimental and was built with much ❤️ and vibe coding. Please do not launch 🚀 or perform 🧠 surgery using it. (Should be 🅰️-🆗 for your Table-Top application though!)

Features

Wyrdbound Dice supports an extensive range of dice rolling mechanics used across many tabletop RPG systems:

Basic Dice Rolling

  • Standard polyhedral dice: 1d4, 1d6, 1d8, 1d10, 1d12, 1d20, 1d100
  • Multiple dice: 3d6, 4d8, etc.
  • Percentile dice: 1d% (displays as [tens, ones])

Mathematical Operations

  • Arithmetic operations: 2d6 + 3, 1d20 - 2, 1d6 × 4, 1d10 ÷ 2
  • Complex expressions: 2d6 + 1d4 × 2 - 1
  • Proper precedence: Mathematical order of operations (PEMDAS/BODMAS)
  • Unicode operators: Support for ×, ÷, −, and fullwidth characters
  • Every term is joined by an operator: 3d6 + 5d8, not 3d6 5d8. An expression must be complete — any text the grammar does not describe is a ParseError, never ignored

Keep/Drop Mechanics

  • Keep highest: 4d6kh3 (ability score generation), 2d20kh1 (advantage)
  • Keep lowest: 4d6kl3, 2d20kl1 (disadvantage)
  • Drop operations: 4d6dh1 (drop highest), 4d6dl1 (drop lowest)
  • Multiple operations: 5d6kh3kl1 (chain keep/drop operations)

Reroll Mechanics

  • Unlimited rerolls: 1d6r<=2 (reroll while ≤ 2)
  • Limited rerolls: 1d6r1<=2 (reroll once), 1d6r3<=3 (reroll up to 3 times)
  • Comparison operators: <=, <, >=, >, =
  • Alternate notation: 1d6ro<=2 (reroll once)
  • Combine with keep/drop: 4d6r<=2kh3 (reroll ≤2, keep highest 3)

Exploding Dice

  • Simple explosion: 1d6e (explode on max value)
  • Explicit threshold: 1d6e6, 1d10e>=8
  • Custom conditions: 1d6e>=5 (explode on 5 or 6)
  • Multiple explosions: Dice can explode repeatedly

Fudge Dice (Fate Core/Accelerated)

  • Single Fudge die: 1dF (results: -1, 0, +1)
  • Standard Fate roll: 4dF
  • Symbol display: Shows as -, B, +
  • Math operations: Can be combined with other dice and modifiers

System Shorthands

  • FUDGE: 4dF (Fate Core)
  • BOON: 3d6kh2 (Traveller advantage)
  • BANE: 3d6kl2 (Traveller disadvantage)
  • FLUX: 1d6 - 1d6 (Traveller flux)
  • GOODFLUX: Always positive flux (highest 1d6 - lowest 1d6)
  • BADFLUX: Always negative flux (lowest 1d6 - highest 1d6)
  • PERC / PERCENTILE: 1d%

Shorthands combine with each other and with ordinary terms (FUDGE + BOON, BOON + 2), matched as whole words in any case. GOODFLUX and BADFLUX are not arithmetic and must be the whole expression.

Named Modifiers

  • Static modifiers: {"Strength": 3, "Proficiency": 2}
  • Dice modifiers: {"Guidance": "1d4", "Bane": "-1d4"}
  • Mixed modifiers: Combine static numbers and dice expressions

Advanced Features

  • Zero dice handling: 0d6 returns 0
  • Negative dice: -1d6 returns negative result
  • Thread safety: Safe for concurrent use
  • Error handling: Clear exceptions for invalid conditions
  • Infinite condition detection: Prevents impossible reroll/explode scenarios

Installation

For End Users

pip install wyrdbound-dice

Note: This package is experimental and its API may still change between releases. See CHANGELOG.md for what moved.

For Development

If you want to contribute to the project or use the latest development version:

# Clone the repository
git clone https://github.com/wyrdbound/wyrdbound-dice.git
cd wyrdbound-dice

# Install in development mode
pip install -e .

Optional Dependencies

For visualization features (graph tool):

pip install "wyrdbound-dice[visualization]"

For development:

pip install -e ".[dev]"

For both visualization and development:

pip install -e ".[dev,visualization]"

Quick Start

from wyrdbound_dice import Dice

# Basic roll
result = Dice.roll("1d20")
print(result.total) # 20
print(result)       # 20 = 20 (1d20: 20)

# Complex expression
result = Dice.roll("2d6 + 1d4 × 2 + 3")
print(result) # 17 = 8 (2d6: 6, 2) + 3 (1d4: 3) x 2 + 3

# Advantage roll (D&D 5e)
result = Dice.roll("2d20kh1")
print(result) # 19 = 19 (2d20kh1: 19, 12)

# Reroll (D&D 5e - Great Weapon Fighting)
result = Dice.roll("2d6r1<=2")
print(result) # 12 = 12 (2d6r1<=2: 1, 2, 6, 6)

# Exploding dice (Savage Worlds)
result = Dice.roll("1d6e")
print(result) # 11 = 11 (1d6e6: 6, 5)

# Fate Core
result = Dice.roll("4dF + 2")
print(result) # 2 = 0 (4dF: +, B, -, B) + 2

# With named modifiers
modifiers = {"Strength": 3, "Proficiency": 2, "Bless": "1d4"}
result = Dice.roll("1d20", modifiers)
print(result) # 20 = 12 (1d20: 12) + 3 (Strength) + 2 (Proficiency) + 3 (Bless: 3 = 3 (1d4: 3))

API Reference

Main Classes

Dice

The main entry point for dice rolling.

Dice.roll(expression, modifiers=None)

  • expression (str): Dice expression to evaluate
  • modifiers (dict, optional): Named modifiers as {name: value} where value can be int or dice expression string
  • Returns: RollResultSet object

Dice.validate(expression) — also wyrdbound_dice.validate(expression)

Checks an expression without rolling it. It runs exactly the checks roll runs before rolling, and nothing else, so an expression that validates is one roll accepts. No dice are rolled and no randomness is drawn — use it wherever an expression arrives before it is needed, such as a game-system loader or a form field.

  • Returns: None
  • Raises ParseError for a malformed expression, text the grammar does not describe, or a broken input limit; InfiniteConditionError for a reroll or explode condition that matches every face; DivisionByZeroError for a divisor that contains no dice and is zero (1d6 / 0). A divisor with dice, like 1d6 / (1d2 - 1), is zero only on some rolls and can only be found by rolling.
from wyrdbound_dice import ParseError, validate

validate("4d6kh3 + 2")        # returns None
try:
    validate("1d20+{{ bonus }}")
except ParseError as e:
    print(e)  # Invalid character '{' at position 5

RollResultSet

Contains the results of a dice roll.

Properties:

  • total (int): Final calculated result
  • results (list): List of individual RollResult objects
  • modifiers (list): List of applied modifiers
  • __str__(): Human-readable description of the complete roll

RollResult

Represents a single dice expression result.

Properties:

  • num (int): Number of dice rolled
  • sides (int/str): Number of sides (or "F" for Fudge, "%" for percentile)
  • rolls (list): Final kept dice values
  • all_rolls (list): All dice rolled (including rerolls, explosions)
  • total (int): Sum of kept dice

Exceptions

  • ParseError: Invalid dice expression syntax
  • DivisionByZeroError: Division by zero in expression
  • InfiniteConditionError: Impossible reroll/explode condition

Command Line Tools

Roll Tool

Roll dice expressions from the command line:

# Basic usage
python tools/roll.py "1d20 + 5"

# Multiple rolls
python tools/roll.py "2d6" --count 10

# JSON output (single roll)
python tools/roll.py "1d20" --json

# JSON output (multiple rolls)
python tools/roll.py "1d6" --count 3 --json

# Validate without rolling
python tools/roll.py "4d6kh3 + 2" --check          # prints "valid", exit 0
python tools/roll.py "2d6 banana" --check --json   # {"valid": false, "error": "..."}, exit 1

Options:

  • -v, --verbose: Show detailed breakdown
  • -n, --count N: Roll N times
  • --json: Output results as JSON
  • --check: Validate the expression without rolling it. Prints valid (exit 0), or the error on stderr (exit 1); with --json, {"valid": true} or {"valid": false, "error": "..."}

JSON Output Format:

Single roll returns an object:

{
  "result": 14,
  "description": "14 = 14 (1d20: 14)"
}

Multiple rolls return an array:

[
  {
    "result": 4,
    "description": "4 = 4 (1d6: 4)"
  },
  {
    "result": 6,
    "description": "6 = 6 (1d6: 6)"
  }
]

Visualization Tool

Generate probability distributions and statistics:

# Basic distribution graph
python tools/graph.py "2d6"

# Complex expression with more samples
python tools/graph.py "1d20 + 5" --num-rolls 50000

# Specify output file
python tools/graph.py "4d6kh3" --output ability_scores.html

Features:

  • Probability distribution histograms
  • Statistical analysis (mean, mode, range)
  • Comparison charts for multiple expressions
  • Export to various image formats

Supported Systems

WyrdBound Dice has been designed to support mechanics from many popular RPG systems:

  • D&D 5e / Pathfinder: Advantage/disadvantage (2d20kh1/2d20kl1), ability scores (4d6kh3)
  • Savage Worlds: Exploding dice (1d6e), wild dice, aces
  • Fate Core/Accelerated: Fudge dice (4dF, FUDGE)
  • Traveller: Boon/Bane (BOON/BANE), Flux dice (FLUX)
  • World of Darkness: Dice pools with success counting (upcoming)
  • Shadowrun: Exploding dice, glitch detection (upcoming)

Examples

Character Creation

# D&D 5e ability scores
stats = []
for _ in range(6):
    result = Dice.roll("4d6kh3")
    stats.append(result.total)

# Traveller characteristics with modifiers
characteristics = Dice.roll("2d6", {"DM": 1})

Combat Rolls

# D&D 5e attack with advantage
attack = Dice.roll("2d20kh1 + 8")  # +8 attack bonus

# Savage Worlds damage with ace
damage = Dice.roll("1d6e + 2")

# Fate Core with aspects
fate_roll = Dice.roll("4dF + 3", {"Aspect": 2})

Complex Expressions

# Fireball damage (8d6) with Metamagic (reroll 1s)
fireball = Dice.roll("8d6r1<=1")

# Sneak attack with multiple damage types
sneak = Dice.roll("1d8 + 3d6")  # Rapier + sneak attack

# Mathematical complexity
complex_formula = Dice.roll("(2d6 + 3) × 2 + 1d4 - 1")

Debug Logging

WyrdBound Dice includes comprehensive debug logging to help troubleshoot dice rolling issues and understand how expressions are parsed and evaluated.

Enabling Debug Mode

from wyrdbound_dice import Dice

# Enable debug logging for a roll
result = Dice.roll("2d6 + 3", debug=True)

Debug Output Example

When debug mode is enabled, you'll see detailed step-by-step information:

DEBUG: [START] Rolling expression: '2d6 + 3'
DEBUG: [PROCESSING] Starting expression processing
DEBUG: NORMALIZED: '2d6 + 3'
DEBUG: [PARSER_SELECTION] Using precedence parser
DEBUG: [TOKENIZING] Tokenizing expression: '2d6 + 3'
DEBUG: Tokens: ['DICE(2d6)@0', 'PLUS(+)@3', 'NUMBER(3)@4']
DEBUG: [PARSING] Parsing tokens with precedence rules
DEBUG: [EVALUATING] Evaluating parsed expression
DEBUG: Rolling 1d6: 5
DEBUG: Rolling 1d6: 4
DEBUG: [RESULT] Expression evaluated to: 12
DEBUG: TOTAL 12 modifiers(0) = 12
DEBUG: [COMPLETE] Final result: 12

What Debug Mode Shows

Debug logging provides insights into:

  • Expression normalization: How input expressions are cleaned and processed
  • Shorthand expansion: When shortcuts like "FUDGE" are expanded to "4dF"
  • Parser selection: Whether the precedence parser or original parser is used
  • Tokenization: How complex expressions are broken into tokens
  • Individual dice rolls: Each die roll with specific results
  • Keep/drop operations: Parsed keep/drop operations like "kh2"
  • Mathematical evaluation: Step-by-step calculation of complex expressions
  • Modifier processing: How modifiers are applied to results
  • Error handling: Debug information even when errors occur

Debug Examples

# Simple dice with debug
result = Dice.roll("1d20", debug=True)

# Complex expression with debug
result = Dice.roll("2d6 * 2 + 1d4", debug=True)

# Keep operations with debug
result = Dice.roll("4d6kh3", debug=True)

# Shorthand expansion with debug
result = Dice.roll("FUDGE", debug=True)

# With modifiers and debug
modifiers = {"strength": 3, "magic_bonus": 2}
result = Dice.roll("1d20", modifiers=modifiers, debug=True)

Custom Debug Loggers

You can inject your own logger to capture debug output using Python's standard logging interface:

import logging
from wyrdbound_dice import Dice
from wyrdbound_dice.debug_logger import StringLogger

# Method 1: Use the built-in StringLogger for testing/API purposes
string_logger = StringLogger()
result = Dice.roll("2d6 + 3", debug=True, logger=string_logger)

# Get all the debug output as a string
debug_output = string_logger.get_logs()
print(debug_output)

# Clear the logger for reuse
string_logger.clear()

# Method 2: Use Python's standard logging module
# Create a custom logger with your preferred configuration
logger = logging.getLogger('my_dice_app')
logger.setLevel(logging.DEBUG)

# Add your own handler (file, web service, etc.)
handler = logging.FileHandler('dice_debug.log')
handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
logger.addHandler(handler)

# Use with dice rolling
result = Dice.roll("1d20", debug=True, logger=logger)

# Method 3: Create a custom logger class
class WebAppLogger:
    def debug(self, message):
        # Send to your web app's logging system
        app.logger.debug(message)

    def info(self, message):
        app.logger.info(message)

    def warning(self, message):
        app.logger.warning(message)

    def error(self, message):
        app.logger.error(message)

web_logger = WebAppLogger()
result = Dice.roll("1d20", debug=True, logger=web_logger)

Logger Interface

Custom loggers should implement Python's standard logging interface methods:

class MyCustomLogger:
    def debug(self, message: str) -> None:
        """Log a debug message."""
        ...

    def info(self, message: str) -> None:
        """Log an info message."""
        ...

    def warning(self, message: str) -> None:
        """Log a warning message."""
        ...

    def error(self, message: str) -> None:
        """Log an error message."""
        ...
class MyLogger:
    def log(self, message: str) -> None:
        # Your custom logging implementation
        pass

Command Line Debug

The tools/roll.py script also supports debug mode:

# Basic roll with debug
python tools/roll.py "2d6 + 3" --debug

# Complex expression with debug
python tools/roll.py "4d6kh3" --debug

# Multiple rolls with debug
python tools/roll.py "1d6" -n 3 --debug

# JSON output with debug information included
python tools/roll.py "2d6 + 3" --json --debug

# Help shows all options including debug
python tools/roll.py --help

When using --json --debug, the debug output is captured and included in the JSON response under a "debug" key:

{
  "result": 11,
  "description": "11 = 8 (2d6: 4, 4) + 3",
  "debug": "DEBUG: [START] Rolling expression: '2d6 + 3'\nDEBUG: [PROCESSING] Starting expression processing\n..."
}

Debug Output Format

Debug messages are prefixed with DEBUG: and use structured labels like [START], [TOKENIZING], [PARSING], etc. This makes it easy to follow the progression through the dice rolling engine and identify where issues might occur.

RNG Injection

By default, Dice.roll() uses Python's stdlib random module, producing non-deterministic results. You can supply your own random number source via the rng= parameter — any object with a random() method returning a float in [0.0, 1.0) is accepted (duck-typed).

Seeded Rolls

import random
from wyrdbound_dice import Dice

# Same seed always produces the same result
rng = random.Random(42)
result = Dice.roll("2d6 + 3", rng=rng)
print(result)  # e.g., "11 = 8 (2d6: 5, 3) + 3"

# Replay identically
result2 = Dice.roll("2d6 + 3", rng=random.Random(42))
assert result.total == result2.total  # always True

Deterministic Testing with Mock RNG

from unittest.mock import Mock
from wyrdbound_dice import Dice

# Pin every die to its maximum
max_rng = Mock()
max_rng.random.return_value = 0.9999
result = Dice.roll("1d6", rng=max_rng)
assert result.total == 6

# Control a sequence of rolls
values = iter([0.9, 0.1, 0.5])  # Controls each die in order
seq_rng = Mock()
seq_rng.random.side_effect = lambda: next(values)
result = Dice.roll("3d6", rng=seq_rng)  # rolls: 6, 1, 3 → total 10
assert result.total == 10

Modifier Propagation

When a modifier is itself a dice expression, the same rng is used — the entire call is reproducible from one seed:

import random
from wyrdbound_dice import Dice

rng = random.Random(7)
result = Dice.roll("1d20", modifiers={"Bless": "1d4"}, rng=rng)
# Both the d20 and the Bless d4 use the same rng sequence

CLI: --seed

# Same seed → same result every time
python tools/roll.py "2d6 + 3" --seed 42
# 11 = 8 (2d6: 5, 3) + 3

# JSON output includes seed
python tools/roll.py "1d20" --seed 42 --json
# {"result": 14, "description": "14 = 14 (1d20: 14)", "seed": 42}

# Reproducible batch
python tools/roll.py "4d6kh3" --seed 42 --count 6

Thread Safety

Do not share a single rng instance across threads. Each thread should create its own:

import random, threading
from wyrdbound_dice import Dice

def roll_in_thread(seed):
    rng = random.Random(seed)  # per-thread rng — safe
    print(Dice.roll("2d6", rng=rng).total)

threads = [threading.Thread(target=roll_in_thread, args=(i,)) for i in range(10)]
for t in threads: t.start()
for t in threads: t.join()

Formatting Roll Output

A roll's parts are available as data, and its text rendering is configurable. str(result) always shows the standard rendering; result.format(...) renders it any other way without re-rolling.

Named styles

import random
from wyrdbound_dice import Dice, RollFormat

result = Dice.roll("4d6kh3", rng=random.Random(42))

print(str(result))                              # 8 = 8 (4d6kh3: 4, 1, 2, 2)
print(result.format(RollFormat.STANDARD))       # 8 = 8 (4d6kh3: 4, 1, 2, 2)
print(result.format(RollFormat.MINIMAL))        # 8
print(result.format(RollFormat.COMPACT))        # 8 = 8 (4d6kh3:4,2,2)
print(result.format(RollFormat.VERBOSE))        # 8 = 8 (4d6kh3: 4, ~1~, 2, 2)

VERBOSE marks dice that were dropped with ~...~, so you can see exactly which die the keep/drop chain removed.

Overriding individual fields

RollFormat is a frozen dataclass; use dataclasses.replace to derive a new one.

import dataclasses
from wyrdbound_dice import RollFormat

no_notation = dataclasses.replace(
    RollFormat.STANDARD, die_separator=" ", show_notation=False
)
print(result.format(no_notation))               # 8 = 8 (4 1 2 2)

Fields include show_notation, dropped (Dropped.SHOWN / HIDDEN / MARKED), show_rerolls, modifier_depth, the separators, multiply_symbol, divide_symbol, dropped_marker, fudge_symbols and percentile.

The layout template

layout arranges the three top-level components — {total}, {breakdown} and {expression} — and only those. It is validated when the format is constructed, so a bad layout fails where it was written.

import dataclasses
from wyrdbound_dice import RollFormat

expr_first = dataclasses.replace(RollFormat.STANDARD, layout="{expression}: {total}")
print(result.format(expr_first))                # 4d6kh3: 8

just_total = dataclasses.replace(RollFormat.STANDARD, layout="{total}")
print(result.format(just_total))                # 8

RollFormat(layout="{total} {bogus}")            # raises ValueError

Setting one style application-wide

from wyrdbound_dice import RollFormat, set_default_format, get_default_format

set_default_format(RollFormat.COMPACT)
print(result.format())          # uses COMPACT
print(str(result))              # unchanged: 8 = 8 (4d6kh3: 4, 1, 2, 2)

set_default_format(None)        # back to STANDARD

The default affects only result.format() with no argument. It never changes str(result), and is meant to be set once at startup.

The structured breakdown

result.breakdown is a frozen RollBreakdown — the tree, the per-die faces and their provenance, and the modifiers. to_dict() is directly JSON-serialisable.

breakdown = result.breakdown
print(breakdown.total)                          # 8

import json
print(json.dumps(breakdown.to_dict())[:60])     # {"root": {"type": "dice", "group": {"num": 4, ...

Each die records every face it rolled and where each came from:

group = result.results[0].breakdown
for die in group.dice:
    print(die.value, die.faces, die.sources, die.kept)
# 4 (4,) ('roll',) True
# 1 (1,) ('roll',) False
# 2 (2,) ('roll',) True
# 2 (2,) ('roll',) True

Custom formatters

DefaultFormatter is subclassable; override any format_* hook. A Formatter is any object with a compatible format(breakdown) method.

from wyrdbound_dice import DefaultFormatter, Formatter

class HashFormatter(DefaultFormatter):
    def format_die(self, die, group):
        return "#"

print(HashFormatter().format(result.breakdown))  # 8 = 8 (4d6kh3: #, #, #, #)

class DuckFormatter:
    def format(self, breakdown):
        return f"total={breakdown.total}"

duck = DuckFormatter()
assert isinstance(duck, Formatter)
print(result.format(duck))                       # total=8

CLI flags

python tools/roll.py "4d6kh3" --seed 42                      # standard
python tools/roll.py "4d6kh3" --seed 42 --format minimal     # 8
python tools/roll.py "4d6kh3" --seed 42 --format verbose     # marks dropped dice
python tools/roll.py "4d6kh3" --seed 42 --json --detail      # adds a "breakdown" key

--format accepts standard, compact, minimal and verbose. --json without --detail emits exactly the same keys as before (result, description, and seed when given); --detail adds breakdown.

Input Limits

If you accept dice expressions from anyone other than yourself — a Discord bot, a VTT, a web form — read this section. Rolling dice is unbounded work by nature, and these limits are the library's only protection against a hostile expression. They are deliberately generous: every one sits far above anything a tabletop system asks for.

Limit Value Bounds
MAX_EXPRESSION_LENGTH 1,000 characters The input string itself. Normalization and validation scan it with several regexes, so long inputs cost more than linearly.
MAX_DICE_COUNT 10,000 Dice in a single term, e.g. the 9999 in 9999d6.
MAX_TOTAL_DICE 20,000 Dice across the whole expression. Per-term caps alone leave the sum unbounded: 9999d6+9999d6+… packed to the length limit is 1.43 million dice.
MAX_DIE_SIDES 1,000,000 The size of one die. Python integers are unbounded, so 1d99999999999999999999 otherwise "rolls" a twenty-digit number.

All four raise ParseError. Import them from wyrdbound_dice.dice if you want to surface the ceilings in your own error messages.

MAX_TOTAL_DICE is also enforced at runtime, not just counted up front, because rerolls and explosions add dice as they go. The infinite-condition validator rejects conditions that match every face (1d6e>=1), but a condition matching all but one face is not infinite and is not rejected: 1d1000000e>=2 names a single die and rolls it hundreds of thousands of times. When a roll exhausts the budget it raises InfiniteConditionError.

from wyrdbound_dice import Dice
from wyrdbound_dice.errors import InfiniteConditionError, ParseError

try:
    result = Dice.roll(user_supplied_expression)
except (ParseError, InfiniteConditionError) as exc:
    # Too long, too many dice, too many sides, or a runaway explode condition
    return f"Sorry, I can't roll that: {exc}"

What these limits do not cover

  • Repeated calls. One roll is now bounded work; a thousand of them is not. Rate limiting, request timeouts, a bounded worker pool, and a per-worker memory cap belong in your service, not here.
  • Caller-supplied modifiers. Each entry in the modifiers dict is rolled by its own Dice.roll() call and therefore gets its own budget. If you let users supply modifiers as well as the expression, bound the size of that dict yourself.
  • Memory per die. A roll retains per-die provenance (dice_traces) for the structured breakdown, at roughly 400 bytes per die against 8 bytes for all_rolls. At the limits above that is a few megabytes; it is the reason the aggregate bound is 20,000 rather than something much larger.

Development

Setting Up Development Environment

# Install the package with development dependencies
pip install -e ".[dev]"

# Install with both development and visualization dependencies
pip install -e ".[dev,visualization]"

# Or install development dependencies separately
pip install pytest pytest-cov black isort ruff

Running Tests

# Run all tests
python -m pytest tests/

# Run with coverage
python -m pytest tests/ --cov=wyrdbound_dice

# Run with coverage and generate HTML report
python -m pytest tests/ --cov=wyrdbound_dice --cov-report=html

# Run specific test class
python -m pytest tests/test_dice.py::TestDiceKeepHighestLowest

Code Quality

# Format code
black src/ tests/ tools/

# Sort imports
isort src/ tests/ tools/

# Lint code
ruff check src/ tests/ tools/

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Areas for Contribution

  • New features for existing RPG systems
  • Performance optimizations
  • Additional CLI tools
  • Documentation improvements
  • Bug fixes and testing

Continuous Integration

This project uses GitHub Actions for CI/CD:

  • Testing: Automated tests across Python 3.8-3.12 on Ubuntu, Windows, and macOS
  • Code Quality: Black formatting, isort import sorting, and Ruff linting
  • Package Validation: Installation testing and CLI tool verification

All pull requests are automatically tested and must pass all checks before merging.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Inspired by the diverse mechanics of tabletop RPG systems
  • Thanks to the RPG community for feedback and feature requests
  • Built with mathematical precision and gaming passion

Release files for wyrdbound-dice 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wyrdbound-dice 0.3.0
File Size Uploaded
wyrdbound_dice-0.3.0.tar.gz 81.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wyrdbound-dice 0.3.0
File Interpreter ABI Platform
wyrdbound_dice-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 130.6 kB

Release files / wyrdbound_dice-0.3.0.tar.gz

Download URL wyrdbound_dice-0.3.0.tar.gz
Size 81.6 kB
Tags Source
SHA-256 checksum
How to use checksums
624be08f11d6750ec747b1e0875a9369c1695ebbc820a84c41b80242bcff81bd
BLAKE2b-256 checksum
How to use checksums
201ddfb8ba822fda4ebdc2fb6bf39f95aff1d56d33ecbe87a49b86c6f3c87440
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / wyrdbound_dice-0.3.0-py3-none-any.whl

Download URL wyrdbound_dice-0.3.0-py3-none-any.whl
Size 49.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5949f35665caf1992d3acbee1bbd393fe66242e5f95e2dfd57f40916af041874
BLAKE2b-256 checksum
How to use checksums
99b26cc083541fa2a70d3d6824e71d4423500f680c93c38f6698408adfe891cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.1.0

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page