
Solving Mississippi Marbles: Exact Expected Value and Selective Dice Retention
Jonathan Lamontagne-Kratz
•14 min read
Abstract: This analysis models the exact expected value of every decision point in Mississippi Marbles by replacing the manual, error-prone combinatorial partitioning from my high school Internal Assessment (IA) with exact state-space enumeration ( outcome iteration) and fixed-point value iteration in Python. We derive the exact expected values for different actions during a turn of Mississippi Marbles, and apply them to the original strategy I suggested in my IA.
Author's Note: This blog post is adapted from my International Baccalaureate (IB) SL Mathematics Internal Assessment (IA). Full dataset tables and complete Python script excerpts from the repository package are included in the appendices at the bottom of this article.
Try the Interactive Lab: Want to play Mississippi Marbles against the strategy from this article? Launch the Mississippi Marbles Interactive Solver Lab.
Developing a baseline Mississippi Marbles strategy requires evaluating the exact per-turn expected value of every decision point using state-space enumeration and Expectimax dynamic programming. The need for this analysis grew out of a casual game of Mississippi Marbles with my grandfather that turned into a heated debate about dice probability. I play to win, so I attempted to mathematically solve the game. In high school, I wrote my IB Mathematics Internal Assessment on this topic, but my original manual calculations suffered from double-counting overlaps and artificial simulation busting.
By revisiting this problem with dynamic programming, this analysis provides the corrected expected values and updates the strategy I originally suggested.
Game Overview & Rules
Already know how to play? Skip straight to the math.
Before diving into the mathematics, it is important to understand the fundamental rules and official scoring system of Mississippi Marbles (sourced from A Bit Dicey):
- Equipment: The game uses six 6-sided dice.
- Objective ("The Delta"): The first player to reach or surpass 11,000 points wins the game.
- Opening Threshold ("Makin' Mud"): A player must score at least 700 points in a single turn to officially open ("get on the board").
- Until a player has officially opened, they cannot bank points or piggyback off passed scores. If a player scores all 6 dice before reaching 700 points, they must continue rolling ("hot hand") until they reach 700+ points.
- If an unopened player rolls a non-scoring roll (bust) before reaching 700 points, they earn 0 points and must try again on their next turn. Once officially opened, a player can bank points after any scoring roll on subsequent turns.
- Turn Flow & Hot Hand:
- On a turn, a player rolls the available dice. Any scoring combination must be thrown on a single roll.
- If a roll produces no scoring dice, the player busts and loses all unbanked points accumulated during that turn.
- If all 6 dice become scoring dice in a turn, the player earns a "hot hand" and must roll all 6 dice again, adding to their running total.
- Penalty Rule ("A Flood"):
- If at any time a player rolls 4 twos on a single roll, they trigger A Flood! The player loses ALL total points acquired in the entire game (resetting their score to 0) and must start over on their next turn by reopening with 700 points ("Makin' Mud").
Game Variant Note (Piggy-Back Rule): In this analysis and interactive solver, we implement the Piggy-Back variant defined in the official rules. When a player banks points and passes their turn, the preceding player keeps all points they earned. The next player in line may choose to Piggyback (accepting the remaining dice and passed turn score) or Start Fresh with 6 dice and 0 passed points. Note that a player must already be "on the board" (officially opened with 700+ points) to be eligible to piggyback.
Official Scoring Table
| Combination | Traditional Term | Point Value |
|---|---|---|
| Single 1 | — | 100 pts |
| Single 5 | — | 50 pts |
| 3 Ones | Smooth Water | 500 pts |
| 3 Twos | Smooth Water | 200 pts |
| 3 Threes | Smooth Water | 300 pts |
| 3 Fours | Smooth Water | 400 pts |
| 3 Fives | Smooth Water | 500 pts |
| 3 Sixes | Smooth Water | 600 pts |
| 4 of a Kind (except Twos) | Ridin' the Rapids | 1,000 pts |
| Straight (1-2-3-4-5-6) | Big Muddy | 2,000 pts |
| 5 of a Kind | Channel Cat | 3,000 pts |
| 6 of a Kind | All the Marbles | 6,000 pts |
| 4 Twos | A Flood | Reset Total Game Score to 0 |
| Opening Threshold | Makin' Mud | 700 pts in a single turn |
| Winning Score | The Delta | 11,000 pts |
Single-Roll Probability Calculations
To determine optimal decisions, we first calculate the expected scoring values and probabilities for a single roll across different remaining dice counts (). For the purpose of this analysis, we rely on two core statistical tools:
- Expected Value : The weighted average of all possible outcomes: In the context of dice games, gives us the average score we can expect from a single roll the baseline number that tells us whether rolling again is mathematically worth the risk.
- Exact State-Space Enumeration: Instead of manually computing multinomial coefficients and subtraction rules across multi-category overlaps (which caused double-counting errors in my original high school IA), we iterate through all outcomes directly in Python using multinomial count vectors . Concretely, we enumerate the count vectors (each weighted by its multinomial probability ), which is exactly equivalent to summing over all outcomes.
Notation & Precision:
- Probabilities are calculated to 5 decimal places to ensure exact convergence.
- Expected values are expressed in floating-point precision rounded to 3 decimal places.
Single-Roll Expected Values & Category Breakdowns
For a single die (), the only scoring outcomes are rolling a 1 (100 pts) or a 5 (50 pts), each with probability :
As increases, three-of-a-kind sets, straights, and multi-set combinations emerge.
Table 1: Exact Single-Roll Expected Values (Theoretical vs Monte Carlo)
| Remaining Dice () | Total State Space () | Exact Expected Value | Monte Carlo (10M Trials) | % Difference |
|---|---|---|---|---|
| 1 | 6 | 25.000 | 25.013 | 0.051% |
| 2 | 36 | 50.000 | 50.012 | 0.025% |
| 3 | 216 | 84.491 | 84.553 | 0.073% |
| 4 | 1,296 | 135.802 | 135.832 | 0.022% |
| 5 | 7,776 | 210.487 | 210.638 | 0.072% |
| 6 | 46,656 | 343.643 | 343.714 | 0.021% |
(Source code for exact enumeration and Monte Carlo validation is provided in Appendix A).
Infinite-Roll Expected Values (Expectimax Formulation)
Single-roll expectations assume a player rolls once and immediately banks. In actual gameplay, a player may continue rolling remaining un-scored dice, or reset to 6 fresh dice upon scoring all dice ("hot hand").
We model this decision tree as an Expectimax Dynamic Program. Let represent the expected total score achievable from dice under a given keep policy:
Where is a face-count outcome vector, is its multinomial probability, and is the state transition value:
- Greedy Policy: The player automatically keeps every scoring die. If points are scored, .
- Optimal Policy: The player chooses the legal action maximizing immediate score plus continuation value:
When all dice score (), a "hot hand" occurs, resetting the dice count to 6 and adding recursively. We solve for using fixed-point value iteration until convergence ().
Table 2: Roll-Until-Bust Expected Scores (Greedy vs Optimal)
| Dice Count () | Base Greedy | Monte Carlo (10M Trials) | Optimized Selective | Gain from Optimization |
|---|---|---|---|---|
| 1 | 250.265 | 250.427 | 267.875 | +17.610 |
| 2 | 236.317 | 236.482 | 250.014 | +13.697 |
| 3 | 282.679 | 282.638 | 295.615 | +12.936 |
| 4 | 371.620 | 371.873 | 384.981 | +13.361 |
| 5 | 493.256 | 493.021 | 518.464 | +25.208 |
| 6 | 675.794 | 675.710 | 728.624 | +52.830 |
(Source code for exact recursive solvers is provided in Appendix B).
Refining Selection Rules (Selective Keeping)
The most counter-intuitive finding from our Expectimax solver is that greedily banking all scoring dice lowers total expected return.
When evaluating selective keeping choices, the Total Expected Value of a decision is calculated by summing the locked-in score being kept and the continuation expectation of the remaining dice:
Practical Example: Rolling a 1 and a 5 with 5 Dice
Suppose a player rolls [1, 5, 2, 3, 4] on 5 dice:
-
Option A (Greedy): Keep both 1 & 5 (150 pts kept, 3 dice remaining to roll)
-
Option B (Selective): Keep ONLY the 1 (100 pts kept, 4 dice remaining to roll)
Because , setting aside only the 1 and rolling 4 remaining dice yields a +39.366 point net gain in expected value compared to keeping both scoring dice!
Master Selective Retention Rules
- 1 to 4 Dice: Always keep every scoring die.
- 5 Dice:
- If you roll a
1and a5, keep only the 1 (leaves 4 dice instead of 3; ). - If you roll two
5s, keep only one 5 (leaves 4 dice instead of 3; ). - If you roll a
1and two5s, keep only the 1 (leaves 4 dice instead of 2; ).
- If you roll a
- 6 Dice:
- Keep as few single
5s as possible when no three-of-a-kind combinations are present. - Skip three
2s if you also rolled a single1(keeping only the1leaves 5 dice; ).
- Keep as few single
(Full representative decision scripts are provided in Appendix C).
Game Theory: Piggybacking & Opponent Passing
In Mississippi Marbles, turns are linked across players via the Piggyback Rule.
1. Optimal Piggybacking Thresholds
A player should choose to piggyback off passed points and remaining dice if the passed score plus continuation value exceeds the value of starting fresh with 6 dice ():
Here rounds up to the smallest multiple of 50 that satisfies the inequality, so the threshold never falls below the break-even point (rounding to the nearest 50 would let a neighbor piggyback a passed score whose continuation value is actually lower than starting fresh).
Table 3: Minimum Passed Score Required to Piggyback
| Remaining Dice Received | Unrounded Need () | Minimum Passed Score to Piggyback | |
|---|---|---|---|
| 1 Die | 267.875 | 460.750 | 500 pts |
| 2 Dice | 250.014 | 478.611 | 500 pts |
| 3 Dice | 295.615 | 433.010 | 450 pts |
| 4 Dice | 384.981 | 343.644 | 350 pts |
| 5 Dice | 518.464 | 210.160 | 250 pts |
| 6 Dice | 728.624 | 0.000 | 0 pts (Always Piggyback) |
2. Stopping & Passing Thresholds
Passing a high score with many dice gives your neighbor a statistical advantage. The official passing thresholds dictate when a player should stop rolling and bank points:
Table 4: Passing / Stopping Matrix
| Remaining Dice Passed | Official Stop & Pass Limit | Approx. Single-Step EV Optimal Limit |
|---|---|---|
| 1 Die | 600 pts | 150 pts |
| 2 Dice | 600 pts | 164 pts |
| 3 Dice | 700 pts | 221 pts |
| 4 Dice | 1,900 pts | 321 pts |
| 5 Dice | 5,500 pts | 458 pts |
| 6 Dice | 13,000+ pts (Never Pass 6 Dice) | 656 pts |
(Piggybacking and passing threshold algorithms are provided in Appendix D).
Summary of the Mathematically Optimal Strategy
Master Cheat Sheet for Mississippi Marbles:
- Stopping / Passing:
- Pass with 1 or 2 dice if score pts.
- Pass with 3 dice if score pts.
- Pass with 4 dice if score pts.
- Pass with 5 dice if score pts.
- Never pass 6 dice (unless banking secures an immediate win).
- Piggybacking:
- Piggyback 1 die if passed score pts.
- Piggyback 2 dice if passed score pts.
- Piggyback 3 dice if passed score pts.
- Piggyback 4 dice if passed score pts.
- Piggyback 5 dice if passed score pts.
- Always piggyback 6 dice.
- Selective Dice Retention:
- 1–4 Dice: Take all scoring dice.
- 5 Dice: Keep
1over5; keep only one5when a choice exists.- 6 Dice: Minimize holding single
5s and non-essential sets to preserve dice count for future rolls.
Discussion & Limitations
While exact Expectimax dynamic programming solves the stationary expected-value baseline, real gameplay introduces non-stationary constraints:
- Asymmetric Opponent Behavior: If opponents frequently pass sub-optimal scores (e.g., passing low point totals with 4 or 5 dice remaining), piggybacking thresholds shift dynamically to exploit their errors.
- Endgame Positioning & Score Deficits: Near the 11,000-point winning threshold, maximizing expected value per turn () becomes secondary to maximizing win probability . A player far behind must take high-variance risks, whereas a leading player should bank small scores to deny opponents additional turns.
- Next Steps (Reinforcement Learning): To model dynamic, score-dependent strategies and opponent profiling, we transition from stationary Expectimax to deep Reinforcement Learning in our follow-up research post.
Conclusion & Resources
By revisiting my high school math internal assessment with exact state-space enumeration in Python, we eliminated manual double-counting errors and derived provably correct expected values for Mississippi Marbles. The key insight remains that greedily banking every point lowers expected return, and adopting a selective retention policy yields expected points per turn.
- Interactive Solver: Launch the Mississippi Marbles Solver Lab to play against the EV solver in real-time.
- Source Code Repository: Access the complete Python package on GitHub.
- Follow-Up Research: Read Part 2: Solving the 2-Player Game to see how an AlphaZero agent benchmarks against an exact 2-player MDP solution, adapting to score deficits and endgame positioning.
Appendices: Python Script Excerpts
The scripts below are excerpts from the mississippi-marbles repository package: they import the mississippi_marbles module (implementing the scoring table in scoring.py and the exact solvers in ev.py) rather than being self-contained. Each shows its real output; Appendix D is shown with the corrected ceiling rounding described above.
Appendix A: Python Code for Single-Roll EV
Excerpt: scripts/single_roll_ev.py
"""Table 1: single-roll expected values (exact + Monte Carlo).
Also prints a per-category probability/EV breakdown for every dice count.
Usage:
python scripts/single_roll_ev.py [--trials N] [--seed S]
"""
import argparse
import os
import random
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mississippi_marbles.ev import count_vector_outcomes, expand_counts
from mississippi_marbles.scoring import SINGLE_VALUE, SET_VALUE, score_roll
def exact_single_roll(n, flood=False):
total = 0.0
for vector, prob in count_vector_outcomes(n):
points, _, status = score_roll(expand_counts(vector), flood)
if status not in ("bust", "flood"):
total += prob * points
return total
def monte_carlo_single_roll(n, trials, flood=False, rng=None):
rng = rng if rng is not None else random.Random()
total = 0
for _ in range(trials):
dice = [rng.randint(1, 6) for _ in range(n)]
points, _, status = score_roll(dice, flood)
if status not in ("bust", "flood"):
total += points
return total / trials
def breakdown(vector):
"""Return (label, points) for the greedy score of a count vector."""
counts = list(vector)
n = sum(counts)
if n == 6 and counts == [1, 1, 1, 1, 1, 1]:
return "Straight", 2000
parts = []
singles = 0
for face in range(1, 7):
count = counts[face - 1]
if face in SINGLE_VALUE:
if count >= 3:
parts.append((face, SET_VALUE[face][count]))
elif count > 0:
singles += SINGLE_VALUE[face] * count
elif count >= 3:
parts.append((face, SET_VALUE[face][count]))
if singles == 0 and not parts:
return "Bust", 0
if parts:
parts.sort(key=lambda item: item[1], reverse=True)
face, pts = parts[0]
label = f"{['1s', '2s', '3s', '4s', '5s', '6s'][face - 1]} x3+"
return label, singles + sum(p for _, p in parts)
return "Singles (1s/5s)", singles
def category_table(n, flood=False):
table = {}
for vector, prob in count_vector_outcomes(n):
label, points = breakdown(vector)
row = table.setdefault(label, [0.0, 0.0])
row[0] += prob
row[1] += prob * points
return table
def main():
parser = argparse.ArgumentParser(description="Single-roll expected values.")
parser.add_argument("--trials", type=int, default=10_000_000)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument(
"--flood", action="store_true", help="enable the 4-twos Flood penalty"
)
args = parser.parse_args()
rng = random.Random(args.seed)
print("TABLE 1: SINGLE-ROLL EXPECTED VALUES")
print(f"{'Dice':>5} {'Exact':>10} {'Monte Carlo':>14} {'% Diff':>10}")
for n in range(1, 7):
exact = exact_single_roll(n, args.flood)
mc = monte_carlo_single_roll(n, args.trials, args.flood, rng)
pct = abs(mc - exact) / exact * 100
print(f"{n:>5} {exact:>10.3f} {mc:>14.3f} {pct:>9.3f}%")
print()
print("SCORING CATEGORY BREAKDOWNS (probability, expected value)")
for n in range(1, 7):
print(f"\n{n} dice:")
for label, (prob, contrib) in sorted(category_table(n, args.flood).items()):
print(f" {label:<16} p={prob:.5f} EV contribution={contrib:.3f}")
if __name__ == "__main__":
main()
# Output (table only):
# TABLE 1: SINGLE-ROLL EXPECTED VALUES
# Dice Exact Monte Carlo % Diff
# 1 25.000 25.013 0.051%
# 2 50.000 50.012 0.025%
# 3 84.491 84.553 0.073%
# 4 135.802 135.832 0.022%
# 5 210.487 210.638 0.072%
# 6 343.643 343.714 0.021%
Appendix B: Python Code for Recursive EV
Excerpt: scripts/recursive_ev.py
"""Tables 2 & 3: greedy "roll until bust" expected scores (exact + Monte Carlo).
Simulates a turn where the bot keeps every scoring die and keeps rolling until
it busts (scoring all 6 dice triggers the hot hand reset to 6 dice). Exact
values come from the recursive solver; the Monte Carlo run uses the same rules.
Usage:
python scripts/recursive_ev.py [--trials N] [--seed S]
"""
import argparse
import os
import random
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mississippi_marbles.ev import count_vector_outcomes, expected_value, expand_counts
from mississippi_marbles.scoring import score_roll
def simulate_turn(n, rng, flood=False):
score = 0
dice = n
while True:
roll = [rng.randint(1, 6) for _ in range(dice)]
points, left, status = score_roll(roll, flood)
if status in ("bust", "flood"):
return score
score += points
dice = 6 if left == 0 else left
def monte_carlo_recursive(n, trials, flood=False, rng=None):
rng = rng if rng is not None else random.Random()
total = 0
for _ in range(trials):
total += simulate_turn(n, rng, flood)
return total / trials
def first_roll_breakdown(ev_greedy):
"""First-roll outcomes for a turn starting with n dice."""
rows = {}
for n in range(1, 7):
table = {}
for vector, prob in count_vector_outcomes(n):
dice = expand_counts(vector)
points, left, status = score_roll(dice)
if status in ("bust", "flood"):
label = "Bust"
continuation = 0.0
else:
label = f"{points} pts / {left} dice left"
continuation = ev_greedy[6] if left == 0 else ev_greedy[left]
row = table.setdefault(label, [0.0, 0.0])
row[0] += prob
row[1] += prob * (points + continuation)
rows[n] = table
return rows
def main():
parser = argparse.ArgumentParser(description="Roll-until-bust expected scores.")
parser.add_argument("--trials", type=int, default=10_000_000)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument(
"--flood", action="store_true", help="enable the 4-twos Flood penalty"
)
args = parser.parse_args()
rng = random.Random(args.seed)
exact = expected_value("greedy", args.flood)
print("TABLES 2 & 3: EXPECTED SCORE WHEN ROLLING UNTIL BUST (greedy)")
print(f"{'Dice':>5} {'Exact':>10} {'Monte Carlo':>14} {'% Diff':>10}")
for n in range(1, 7):
mc = monte_carlo_recursive(n, args.trials, args.flood, rng)
pct = abs(mc - exact[n]) / exact[n] * 100
print(f"{n:>5} {exact[n]:>10.3f} {mc:>14.3f} {pct:>9.3f}%")
print()
print("FIRST-ROLL BREAKDOWNS (probability, net EV)")
for n, table in first_roll_breakdown(exact).items():
print(f"\n{n} dice:")
for label, (prob, net) in sorted(table.items(), key=lambda item: -item[1][0]):
print(f" {label:<24} p={prob:.5f} net EV={net:.3f}")
if __name__ == "__main__":
main()
# Output (table only):
# TABLES 2 & 3: EXPECTED SCORE WHEN ROLLING UNTIL BUST (greedy)
# Dice Exact Monte Carlo % Diff
# 1 250.265 250.427 0.065%
# 2 236.317 236.482 0.070%
# 3 282.679 282.638 0.014%
# 4 371.620 371.873 0.068%
# 5 493.256 493.021 0.048%
# 6 675.794 675.710 0.012%
Appendix C: Python Code for Selective Keeping
Excerpt: scripts/selective_keeping.py
"""Table 4: optimized selective-retention expected values.
Compares the greedy keep-everything bot against the optimal policy that picks
the best legal keep at every roll, then prints the blog's representative
decisions and confirms the distilled 5-dice / 6-dice retention rules.
Usage:
python scripts/selective_keeping.py
"""
import os
import sys
from itertools import product
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mississippi_marbles.ev import expected_value
from mississippi_marbles.scoring import counts_of, face_options
FACE_NAME = ["", "1s", "2s", "3s", "4s", "5s", "6s"]
REPRESENTATIVE_ROLLS = [
("5 dice: roll a 1 and a 5 (rule: keep only the 1)", [1, 5, 2, 3, 4]),
("5 dice: roll two 5s (rule: keep only one 5)", [5, 5, 2, 3, 4]),
("5 dice: roll 1 + two 5s (rule: keep only the 1)", [1, 5, 5, 2, 3]),
("5 dice: roll two 1s (kept: both 1s)", [1, 1, 2, 3, 4]),
("6 dice: single 1 and 5, no sets (rule: keep only the 1)", [1, 5, 2, 3, 4, 4]),
("6 dice: three 2s + a single 1 (rule: skip the 2s)", [2, 2, 2, 1, 4, 6]),
("6 dice: three 2s + a 1 and a 5 (rule: keep the 2s too)", [2, 2, 2, 1, 5, 3]),
]
def best_keep(dice, ev_opt):
counts = counts_of(dice)
n = len(dice)
if n == 6 and counts == [1, 1, 1, 1, 1, 1]:
kept_faces = [(face, count) for face, count in enumerate(counts, 1) if count]
return kept_faces, 2000, "hot", 2000 + ev_opt[6]
per_face = [face_options(face, counts[face - 1]) for face in range(1, 7)]
best = ([], 0, "bust", 0.0)
best_value = float("-inf")
for combo in product(*per_face):
kept = sum(option[0] for option in combo)
points = sum(option[1] for option in combo)
if kept == 0:
continue
status = "hot" if kept == n else "keep"
continuation = ev_opt[6] if kept == n else ev_opt[n - kept]
value = points + continuation
if value > best_value:
best_value = value
kept_faces = [
(face, combo[face - 1][0])
for face in range(1, 7)
if combo[face - 1][0] > 0
]
best = (kept_faces, points, status, value)
return best
def main():
greedy = expected_value("greedy")
optimal = expected_value("optimal")
print("TABLE 4: OPTIMIZED STRATEGY EXPECTED VALUES")
print(f"{'Dice':>5} {'Base (greedy)':>15} {'Optimized':>12} {'Gain':>8}")
for n in range(1, 7):
print(
f"{n:>5} {greedy[n]:>15.3f} {optimal[n]:>12.3f} "
f"{optimal[n] - greedy[n]:>8.3f}"
)
print()
print("OPTIMAL KEEP DECISIONS ON REPRESENTATIVE ROLLS")
for description, dice in REPRESENTATIVE_ROLLS:
kept_faces, points, status, value = best_keep(dice, optimal)
kept_desc = (
" + ".join(f"{count} {FACE_NAME[face]}" for face, count in kept_faces)
or "nothing"
)
print(f"\n{description}")
print(f" roll : {dice}")
print(
f" keep : {kept_desc} ({points} pts, {status}) "
f"value = {points} + E(remaining) = {value:.3f}"
)
if __name__ == "__main__":
main()
# Output (table only):
# TABLE 4: OPTIMIZED STRATEGY EXPECTED VALUES
# Dice Base (greedy) Optimized Gain
# 1 250.265 267.875 17.610
# 2 236.317 250.014 13.697
# 3 282.679 295.615 12.936
# 4 371.620 384.981 13.361
# 5 493.256 518.464 25.208
# 6 675.794 728.624 52.830
Appendix D: Python Code for Game Theory Thresholds
Excerpt: scripts/game_theory.py
"""Tables 5 & 6: piggybacking and passing thresholds.
Piggyback (Table 5): accept a passed turn when
passed_score + E_opt(dice) >= E_opt(6)
so the minimum passed score per dice count is the smallest multiple of 50 that
satisfies the inequality:
ceil50(max(0, E_opt(6) - E_opt(dice))).
We round UP (not to the nearest 50): rounding down would admit piggybacks
whose continuation value is actually below starting fresh.
Passing (Table 6): the reference bot's stop-and-pass limits
[600, 600, 700, 1900, 5500, 13000]
plus a one-step EV-optimal comparison for transparency.
Usage:
python scripts/game_theory.py
"""
import math
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mississippi_marbles.ev import count_vector_outcomes, expected_value, expand_counts
from mississippi_marbles.scoring import score_roll
PASS_LIMITS = [600, 600, 700, 1900, 5500, 13000]
def ceil_to_50(x):
return int(math.ceil(x / 50.0) * 50)
def piggyback_thresholds(ev_opt):
thresholds = []
for n in range(1, 7):
need = max(0.0, ev_opt[6] - ev_opt[n])
thresholds.append(ceil_to_50(need))
return thresholds
def one_step_stop_limits(ev_greedy):
"""Approximate EV-optimal stop limit: bank when rolling once more has
non-positive expected marginal value against the greedy continuation."""
limits = []
for n in range(1, 7):
expected = 0.0
bust_prob = 0.0
for vector, prob in count_vector_outcomes(n):
dice = expand_counts(vector)
points, left, status = score_roll(dice)
if status == "bust":
bust_prob += prob
continue
continuation = ev_greedy[6] if left == 0 else ev_greedy[left]
expected += prob * (points + continuation)
limits.append(int(round(expected / (1.0 + bust_prob))))
return limits
def main():
greedy = expected_value("greedy")
optimal = expected_value("optimal")
print("TABLE 5: MINIMUM PASSED SCORE REQUIRED TO PIGGYBACK")
print(
f"{'Dice':>5} {'E_opt(dice)':>12} {'E_opt(6) - E_opt(d)':>20} "
f"{'Min Passed':>12}"
)
thresholds = piggyback_thresholds(optimal)
for n in range(1, 7):
print(
f"{n:>5} {optimal[n]:>12.3f} "
f"{optimal[6] - optimal[n]:>20.3f} {thresholds[n - 1]:>12}"
)
print()
print("TABLE 6: PASSING / STOP-AND-PASS THRESHOLDS")
print(f"{'Dice':>5} {'Stop & Pass':>14} {'EV-optimal (approx)':>20}")
derived = one_step_stop_limits(greedy)
for n in range(1, 7):
print(f"{n:>5} {PASS_LIMITS[n - 1]:>14} {derived[n - 1]:>20}")
if __name__ == "__main__":
main()
# Output:
# TABLE 5: MINIMUM PASSED SCORE REQUIRED TO PIGGYBACK
# Dice E_opt(dice) E_opt(6) - E_opt(d) Min Passed
# 1 267.875 460.750 500
# 2 250.014 478.611 500
# 3 295.615 433.010 450
# 4 384.981 343.644 350
# 5 518.464 210.160 250
# 6 728.624 0.000 0
#
# TABLE 6: PASSING / STOP-AND-PASS THRESHOLDS
# Dice Stop & Pass EV-optimal (approx)
# 1 600 150
# 2 600 164
# 3 700 221
# 4 1900 321
# 5 5500 458
# 6 13000 656