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.
📣 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, not3d6 5d8. An expression must be complete — any text the grammar does not describe is aParseError, 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:
0d6returns 0 - Negative dice:
-1d6returns 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 evaluatemodifiers(dict, optional): Named modifiers as{name: value}where value can be int or dice expression string- Returns:
RollResultSetobject
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
ParseErrorfor a malformed expression, text the grammar does not describe, or a broken input limit;InfiniteConditionErrorfor a reroll or explode condition that matches every face;DivisionByZeroErrorfor a divisor that contains no dice and is zero (1d6 / 0). A divisor with dice, like1d6 / (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 resultresults(list): List of individualRollResultobjectsmodifiers(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 rolledsides(int/str): Number of sides (or "F" for Fudge, "%" for percentile)rolls(list): Final kept dice valuesall_rolls(list): All dice rolled (including rerolls, explosions)total(int): Sum of kept dice
Exceptions
ParseError: Invalid dice expression syntaxDivisionByZeroError: Division by zero in expressionInfiniteConditionError: 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. Printsvalid(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
modifiersdict is rolled by its ownDice.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 forall_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)
| File | Size | Uploaded | |
|---|---|---|---|
| wyrdbound_dice-0.3.0.tar.gz | 81.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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