Skip to content

Instantly share code, notes, and snippets.

@escalonn
Created June 28, 2026 09:48
Show Gist options
  • Select an option

  • Save escalonn/e7da612121ac836a74270b5a7579745d to your computer and use it in GitHub Desktop.

Select an option

Save escalonn/e7da612121ac836a74270b5a7579745d to your computer and use it in GitHub Desktop.
import argparse
import sys
# --------------------------- Payoff table ---------------------------
PAYOFF = {
0: 0,
1: 1000,
2: 2000,
3: 3000,
4: 7500,
5: 12000,
6: 20000,
7: 36000,
8: 60000,
9: 100000,
10: 160000,
}
def main():
parser = argparse.ArgumentParser(description="Trials of Swordmancy game advisor")
parser.add_argument(
"deck",
help='Deck composition as five digits (e.g., "55586" means 1:5, 2:5, 3:5, 4:8, 5:6)',
)
parser.add_argument(
"--trials", type=int, default=3, help="Number of trials remaining (default 3)"
)
parser.add_argument(
"--abandons",
type=int,
default=3,
help="Number of abandons remaining (default 3)",
)
parser.add_argument(
"--doubles", type=int, default=2, help="Number of doubles remaining (default 2)"
)
args = parser.parse_args()
if len(args.deck) != 5 or not args.deck.isdigit():
sys.exit("Deck must be exactly five digits, e.g. 55586")
deck = {i + 1: int(d) for i, d in enumerate(args.deck)}
max_trials = args.trials
max_abandons = args.abandons
max_doubles = args.doubles
full_counts = tuple(deck[i] for i in range(1, 6))
# Compute maximum possible total reward for field width
max_total = max(PAYOFF.values()) * (max_trials + min(max_doubles, max_trials))
rew_digits = len(str(max_total))
act_width = 16
# ---------------- Dynamic Programming cache ----------------
hand_cache = {}
V_start_cache = {}
def hand_value(sum_hand, n, counts, doubled, t, a, d):
"""Returns (max expected total future reward, optimal action string)."""
key = (sum_hand, n, counts, doubled, t, a, d)
if key in hand_cache:
return hand_cache[key]
best_val = -float("inf")
best_act = None
# 1. Start
effective_sum = sum_hand % 11
reward = PAYOFF[effective_sum] * (2 if doubled else 1)
next_v = V_start_cache[(t - 1, a, d)][0] if t > 0 else 0
val_start = reward + next_v
best_val, best_act = val_start, "start"
# 2. Abandon (if available)
if a > 0:
next_d = d + 1 if doubled else d
val_abandon = V_start_cache[(t, a - 1, next_d)][0]
if val_abandon > best_val:
best_val, best_act = val_abandon, "abandon"
# 3. Draw
total_cards = sum(counts)
if n < 5 and total_cards > 0:
exp_val = 0.0
for v in range(1, 6):
cnt = counts[v - 1]
if cnt == 0:
continue
prob = cnt / total_cards
new_sum = sum_hand + v
new_counts = list(counts)
new_counts[v - 1] -= 1
new_counts = tuple(new_counts)
child_val, _ = hand_value(new_sum, n + 1, new_counts, doubled, t, a, d)
exp_val += prob * child_val
if exp_val > best_val:
best_val, best_act = exp_val, "draw"
# 4. Double
if n < 3 and not doubled and d > 0:
val_double, _ = hand_value(sum_hand, n, counts, True, t, a, d - 1)
if val_double > best_val:
best_val, best_act = val_double, "double"
hand_cache[key] = (best_val, best_act)
return best_val, best_act
# Fill V_start_cache
for t in range(max_trials + 1):
for a in range(max_abandons + 1):
for d in range(max_doubles + 1):
if t == 0:
V_start_cache[(0, a, d)] = (0.0, None)
else:
val, act = hand_value(0, 0, full_counts, False, t, a, d)
V_start_cache[(t, a, d)] = (val, act)
# ---------------- Probability of achieving max_total ----------------
prob_target_cache = {}
def prob_of_target(sum_hand, n, counts, doubled, t, a, d, target):
"""Probability that future rewards sum exactly to `target` under optimal EV policy."""
if target < 0:
return 0.0
if t == 0:
return 1.0 if target == 0 else 0.0
key = (sum_hand, n, counts, doubled, t, a, d, target)
if key in prob_target_cache:
return prob_target_cache[key]
# Fetch the optimal action (must already be cached)
_, act = hand_value(sum_hand, n, counts, doubled, t, a, d)
if act == "start":
effective = sum_hand % 11
reward = PAYOFF[effective] * (2 if doubled else 1)
if reward > target:
prob = 0.0
else:
prob = prob_of_target(
0, 0, full_counts, False, t - 1, a, d, target - reward
)
elif act == "abandon":
next_d = d + 1 if doubled else d
prob = prob_of_target(0, 0, full_counts, False, t, a - 1, next_d, target)
elif act == "double":
prob = prob_of_target(sum_hand, n, counts, True, t, a, d - 1, target)
elif act == "draw":
total_cards = sum(counts)
if n >= 5 or total_cards == 0:
prob = 0.0
else:
prob = 0.0
for v in range(1, 6):
cnt = counts[v - 1]
if cnt == 0:
continue
p = cnt / total_cards
new_counts = list(counts)
new_counts[v - 1] -= 1
prob += p * prob_of_target(
sum_hand + v, n + 1, tuple(new_counts), doubled, t, a, d, target
)
else:
prob = 0.0
prob_target_cache[key] = prob
return prob
# ---------------- Interactive play ----------------
t = max_trials
a = max_abandons
d = max_doubles
current_counts = full_counts
hand_sum = 0
n = 0
doubled = False
total_reward_so_far = 0
# Header
print(f"{'Action':<{act_width}} {'Expected':>{rew_digits}} {'P(max)%':>7}")
while t > 0:
val, act = hand_value(hand_sum, n, current_counts, doubled, t, a, d)
total_expected = total_reward_so_far + val
parts = []
while act in ("abandon", "double"):
if act == "abandon":
if doubled:
d += 1
doubled = False
a -= 1
current_counts = full_counts
hand_sum = 0
n = 0
parts.append("ABANDON")
elif act == "double":
doubled = True
d -= 1
parts.append("DOUBLE")
val, act = hand_value(hand_sum, n, current_counts, doubled, t, a, d)
total_expected = total_reward_so_far + val
parts.append(act.upper())
action_str = " & ".join(parts)
# Probability of reaching the absolute maximum overall total
target = max_total - total_reward_so_far
prob = prob_of_target(hand_sum, n, current_counts, doubled, t, a, d, target)
line = f"{action_str:<{act_width}} {round(total_expected):>{rew_digits}} {prob * 100:>7.2f}%"
if act == "start":
print(line)
reward = PAYOFF[hand_sum % 11] * (2 if doubled else 1)
total_reward_so_far += reward
t -= 1
current_counts = full_counts
hand_sum = 0
n = 0
doubled = False
elif act == "draw":
try:
v = int(input(line + " Enter drawn value (1-5): ").strip())
except ValueError:
print("Invalid input. Please enter an integer 1-5.")
continue
if v < 1 or v > 5 or current_counts[v - 1] == 0:
print("Value not in deck or invalid. Try again.")
continue
cnt_list = list(current_counts)
cnt_list[v - 1] -= 1
current_counts = tuple(cnt_list)
hand_sum += v
n += 1
else:
print("Unexpected action.")
break
print(f"Game over. Total reward = {total_reward_so_far}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment