Created
June 28, 2026 09:48
-
-
Save escalonn/e7da612121ac836a74270b5a7579745d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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