Created
February 23, 2025 00:17
-
-
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
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 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