Skip to content

generic-account/dice-dsl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dice DSL

Python eDSL for discrete, turn-based stochastic games. Define games with a small rule subset, compile to IR, and solve with DP or MCTS. Supports probabilistic transitions, multisets for dice/cards, and policy export.

Goals

  • Model discrete, turn-based stochastic games with full information.
  • Express rules declaratively without executing Python at solve-time.
  • Support probabilistic outcomes (dice, cards, tables) and action choice.
  • Provide solver backends and policy export formats (lookup tables, decision trees, Python and C codegen).

Project Structure

  • src/dice_dsl/ core library
    • game.py Game API, state schema, rules
    • state.py variable types and multiset
    • rules.py AST validation
    • ir.py IR types
    • transitions.py rule execution for solvers
    • solvers/ DP, discounted DP, and MCTS
    • exporters/ lookup table, decision tree, codegen
    • dice.py dice sugar and distributions
    • cards.py cards sugar and deck utilities
  • tests/ unit, end-to-end, and matrix tests
  • examples/ example games

Install

Use the local .venv and run everything through it.

.venv/bin/python -m pip install -e ".[dev]"

Quick Start

Define a game, state schema, and rules:

from dice_dsl.game import Game
import dice_dsl.dice as dice

game = Game("SingleDie", num_players=1)
game.register_helper("dice", kind="module")

state = game.state(
    score=game.int(0),
    phase=game.enum(["roll", "done"], "roll"),
)

@game.rule
def roll_once(st):
    if st.phase == "roll":
        st.score = game.chance("roll", dice.uniform_dist([1, 2, 3, 4, 5, 6]))
        game.action_choice("roll")
        st.phase = "done"

Solve with DP or MCTS:

game_ir = game.compile_ir()
from dice_dsl.registry import get_solver
from dice_dsl.solvers import dp  # noqa: F401

solver = get_solver("dp")
policy_ir = solver.solve(game_ir, objective="expected_score", config={"horizon": 1})

Semantics

  • Rules are parsed to AST and compiled to IR. They are not executed at definition time.
  • game.action_choice(...) declares available actions in a rule.
  • game.chance(name, dist) declares a probabilistic transition.
  • Solvers evaluate expected value over chance outcomes.
  • Multisets represent unordered dice/cards with counts.
  • Discounted solving supports ignore_state_fields for unbounded bookkeeping like score, but ignored fields must not affect rule branching or transitions.

Dice Sugar

Dice helpers operate on multisets and distributions:

pool = dice.roll_multiset(5, 6)
keep_max = dice.select_max(pool)
reroll_sel = dice.invert_selection(pool, keep_max)
dist = dice.reroll_dist(pool, reroll_sel, sides=6)

Cards Sugar

Cards helpers use multisets of (rank, suit) and provide common deck/hand operations:

import dice_dsl.cards as cards

deck = cards.full_deck()
hand = cards.select_suit(deck, "S")
dist = cards.draw_dist(deck, n=5)

Example Dice Games

Reroll All But Max

@game.rule
def reroll_except_max(st):
    if st.phase == "roll":
        st.dice_pool = game.chance("roll", dice.roll_dist(5, 6))
        st.phase = "choose"
    elif st.phase == "choose":
        keep = dice.select_max(st.dice_pool)
        reroll = dice.invert_selection(st.dice_pool, keep)
        st.dice_pool = game.chance("reroll", dice.reroll_dist(st.dice_pool, reroll, 6))
        game.action_choice("bank")
        st.phase = "done"

Multiple Actions, Optimize Score

import dice_dsl.dice as dice
from dice_dsl.game import Game
from dice_dsl.policy import Policy
from dice_dsl.registry import get_solver
from dice_dsl.solvers import dp  # noqa: F401

game = Game("RiskyChoice", num_players=1)
game.register_helper("dice", kind="module")

state = game.state(
    score=game.int(0),
    phase=game.enum(["choose", "done"], "choose"),
)

@game.rule
def choose_action(st):
    if st.phase == "choose":
        if game.action_choice("safe"):
            st.score = 1
        if game.action_choice("risky"):
            st.score = game.chance("roll", dice.uniform_dist([0, 3]))
        st.phase = "done"

game_ir = game.compile_ir()
solver = get_solver("dp")
policy_ir = solver.solve(
    game_ir,
    objective="expected_score",
    config={"horizon": 1, "terminal_mode": "value_field", "value_field": "score"},
)
policy = Policy.from_game_ir(policy_ir, game_ir, default_action="safe")
best = policy.best_action(game_ir.initial_state)

Example Card Game

import dice_dsl.cards as cards

game = Game("HighCard", num_players=2)
game.register_helper("cards", kind="module")

state = game.state(
    deck=game.multiset(cards.standard_deck()),
    hand=game.multiset(cards.standard_deck()),
    phase=game.enum(["deal", "score"], "deal"),
)

@game.rule
def deal_once(st):
    if st.phase == "deal":
        st.deck = game.chance(
            "draw",
            cards.draw_step_dist(st.deck, n=1, hand_field="hand", deck_field="deck"),
        )
        st.phase = "score"

Decision Tree Schema

Decision tree exports follow docs/decision_tree.schema.json to guide JSON consumers and code generators.

Testing

  • Unit tests: tests/
  • End-to-end and matrix tests cover backends, objectives, and player counts.

Run all tests:

.venv/bin/python -m pytest

Examples

Run any example directly:

PYTHONPATH=src .venv/bin/python examples/dice_single_probability.py

About

Python eDSL for simulating and automatically solving some probability games

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages