Skip to content

Instantly share code, notes, and snippets.

@mbutler
Created February 23, 2025 00:17
Show Gist options
  • Save mbutler/8f747e38f0a7dc4c123286b42fc83af4 to your computer and use it in GitHub Desktop.
Save mbutler/8f747e38f0a7dc4c123286b42fc83af4 to your computer and use it in GitHub Desktop.
debug the solitaire game Scoundrel to prove how hard if not impossible it is
import random
def simulate_scoundrel(debug=False):
deck = []
for value in range(2, 11):
deck.append((value, 'monster'))
deck.append((value, 'monster'))
deck.append((value, 'weapon'))
deck.append((value, 'potion'))
for value in [11, 12, 13, 14]:
deck.append((value, 'monster'))
deck.append((value, 'monster'))
assert len(deck) == 44, f"Deck size should be 44, got {len(deck)}"
random.shuffle(deck)
health = 20
weapon = None # (value, highest_monster_slain)
used_potion = False
last_avoided = False
room = []
i = 0
if debug:
print(f"Starting Health: {health}, Deck size: {len(deck)}")
while i < len(deck) or room:
if not room:
cards_to_draw = min(4, len(deck) - i)
if cards_to_draw == 0:
break
room = deck[i:i + cards_to_draw]
i += cards_to_draw
else:
cards_to_draw = min(3, len(deck) - i)
if cards_to_draw > 0:
room = [room[0]] + deck[i:i + cards_to_draw]
i += cards_to_draw
else:
room = [room[0]]
cards_to_choose = min(3, len(room) - 1) if len(room) > 1 else len(room)
if debug:
print(f"\nRoom: {room}, Health: {health}, Weapon: {weapon}, Cards to choose: {cards_to_choose}")
if not last_avoided and len(room) == 4 and random.random() < 0.5:
deck.extend(room)
room = []
last_avoided = True
used_potion = False
if debug:
print("Avoided Room")
continue
last_avoided = False
used_potion = False
chosen_indices = set()
# (1) Weapons
weapons_in_room = [(card, idx) for idx, card in enumerate(room) if card[1] == 'weapon']
if weapons_in_room and len(chosen_indices) < cards_to_choose:
best_weapon = max(weapons_in_room, key=lambda x: x[0][0])
weapon_val = best_weapon[0][0]
weapon_idx = best_weapon[1]
if not weapon or weapon_val > weapon[0] or (weapon[1] >= weapon_val):
weapon = (weapon_val, 0)
chosen_indices.add(weapon_idx)
if debug:
print(f"Equipped Weapon {weapon_val}")
# (2) Potions
potions_in_room = [(card, idx) for idx, card in enumerate(room) if card[1] == 'potion' and idx not in chosen_indices]
if potions_in_room and not used_potion and health < 20 and len(chosen_indices) < cards_to_choose:
best_potion = max(potions_in_room, key=lambda x: min(x[0][0], 20 - health))
potion_val = best_potion[0][0]
potion_idx = best_potion[1]
health = min(20, health + potion_val)
chosen_indices.add(potion_idx)
used_potion = True
if debug:
print(f"Used Potion {potion_val}, Health now: {health}")
# (3) Monsters
monsters_in_room = [(card, idx) for idx, card in enumerate(room) if card[1] == 'monster' and idx not in chosen_indices]
while monsters_in_room and len(chosen_indices) < cards_to_choose:
monster_card, monster_idx = min(monsters_in_room, key=lambda x: x[0][0])
monsters_in_room.remove((monster_card, monster_idx))
chosen_indices.add(monster_idx)
monster_val = monster_card[0]
if weapon: # Weapon persists until replaced, fights all Monsters
damage = max(0, monster_val - weapon[0])
health -= damage
weapon = (weapon[0], max(weapon[1], monster_val))
if debug:
print(f"Fought Monster {monster_val} with Weapon {weapon[0]}, Damage: {damage}, Health: {health}")
else:
health -= monster_val
if debug:
print(f"Fought Monster {monster_val} barehanded, Health: {health}")
if health <= 0:
if debug:
print("Health reached 0")
return False
# (4) Remaining
remaining = [idx for idx in range(len(room)) if idx not in chosen_indices]
remaining.sort(key=lambda idx: (room[idx][1] != 'potion', room[idx][0]))
for idx in remaining:
if len(chosen_indices) >= cards_to_choose:
break
card = room[idx]
card_val, card_type = card
if card_type == 'potion' and not used_potion and health < 20:
health = min(20, health + card_val)
used_potion = True
chosen_indices.add(idx)
if debug:
print(f"Used Potion {card_val}, Health now: {health}")
elif card_type == 'weapon':
if not weapon or card_val > weapon[0] or (weapon[1] >= card_val):
weapon = (card_val, 0)
if debug:
print(f"Equipped Weapon {card_val}")
chosen_indices.add(idx)
elif card_type == 'monster':
if weapon:
damage = max(0, card_val - weapon[0])
health -= damage
weapon = (weapon[0], max(weapon[1], card_val))
if debug:
print(f"Fought Monster {card_val} with Weapon {weapon[0]}, Damage: {damage}, Health: {health}")
else:
health -= card_val
if debug:
print(f"Fought Monster {card_val} barehanded, Health: {health}")
chosen_indices.add(idx)
if health <= 0:
if debug:
print("Health reached 0")
return False
if len(room) > cards_to_choose:
leftover_idx = next(idx for idx in range(len(room)) if idx not in chosen_indices)
room = [room[leftover_idx]]
if debug:
print(f"Leftover card: {room[0]}")
else:
room = []
if debug:
print(f"Game ended, Health: {health}")
return health > 0
# Debug one game
print("Debugging one game:")
simulate_scoundrel(debug=True)
# Run simulation
wins = sum(simulate_scoundrel() for _ in range(1000000))
print(f"Win Probability: {wins / 1000000:.4f}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment