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.
- 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).
src/dice_dsl/core librarygame.pyGame API, state schema, rulesstate.pyvariable types and multisetrules.pyAST validationir.pyIR typestransitions.pyrule execution for solverssolvers/DP, discounted DP, and MCTSexporters/lookup table, decision tree, codegendice.pydice sugar and distributionscards.pycards sugar and deck utilities
tests/unit, end-to-end, and matrix testsexamples/example games
Use the local .venv and run everything through it.
.venv/bin/python -m pip install -e ".[dev]"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})- 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_fieldsfor unbounded bookkeeping like score, but ignored fields must not affect rule branching or transitions.
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 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)@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"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)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 exports follow docs/decision_tree.schema.json to guide JSON consumers and code generators.
- Unit tests:
tests/ - End-to-end and matrix tests cover backends, objectives, and player counts.
Run all tests:
.venv/bin/python -m pytestRun any example directly:
PYTHONPATH=src .venv/bin/python examples/dice_single_probability.py