Skip to content

Instantly share code, notes, and snippets.

@ngauthier
Created June 28, 2025 19:50
Show Gist options
  • Save ngauthier/54d34b6d754132ecd525361eec17a256 to your computer and use it in GitHub Desktop.
Save ngauthier/54d34b6d754132ecd525361eec17a256 to your computer and use it in GitHub Desktop.

Scoundrel Rules

Scoundrel is a single-player, rogue-like card game played with a standard deck of cards. The objective is to survive a dungeon crawl by battling monsters and managing your health.

Setup

  1. Prepare the Deck: Start with a standard 52-card deck. Remove all jokers, red face cards (Jacks, Queens, Kings), and red Aces. The remaining cards form the "Dungeon" deck.
  2. Starting Health: You begin with 20 health points.

Card Roles

  • Monsters (Clubs and Spades): These 26 cards represent monsters. Their strength is equal to their face value (2-10), with Jacks at 11, Queens at 12, Kings at 13, and Aces at 14.
  • Weapons (Diamonds): These 9 cards are weapons, with their power equal to their face value.
  • Health Potions (Hearts): These 9 cards are health potions that restore health equal to their face value.

Gameplay

The game is played in a series of turns, each representing a "room" in the dungeon.

  1. Creating a Room: At the start of your turn, draw four cards from the top of the Dungeon deck and place them face up. This is the current room.
  2. Playing the Room: You must choose and resolve three of the four cards in the room, one at a time. The fourth card remains and becomes the first card of the next room.
  3. Resolving Cards:
    • Monster: You can fight a monster barehanded or with an equipped weapon.
      • Barehanded: You defeat the monster, but you take damage equal to its value.
      • With a Weapon: Subtract the weapon's value from the monster's value. The remaining amount is the damage you take. After defeating a monster with a weapon, that weapon can only be used on monsters with a strictly lower value than the one just defeated.
    • Weapon: If you choose a weapon card, you must equip it. Your previous weapon, if any, is discarded.
    • Health Potion: You can use one health potion per turn to restore your health, but not exceeding your starting 20 health. If you draw a second potion in the same room, it is simply discarded.
  4. Skipping a Room: You have the option to skip a room by taking all four cards and placing them at the bottom of the Dungeon deck. However, you cannot skip two rooms in a row.
  5. Ending the Turn: Your turn is complete after you have resolved three of the four cards in the room. The one remaining card stays, and you draw three new cards to form the next room of four cards.

Winning and Losing

  • Losing: The game ends if your health reaches 0.
  • Winning: You win the game if you successfully make it through the entire Dungeon deck.
import random
import os
import re
# ANSI Color Codes
COLORS = {
"RESET": "\033[0m",
"BLACK": "\033[30m",
"RED": "\033[31m",
"GREEN": "\033[32m",
"YELLOW": "\033[33m",
"BLUE": "\033[34m",
"MAGENTA": "\033[35m",
"CYAN": "\033[36m",
"WHITE": "\033[37m",
"BRIGHT_BLACK": "\033[90m",
"BRIGHT_RED": "\033[91m",
"BRIGHT_GREEN": "\033[92m",
"BRIGHT_YELLOW": "\033[93m",
"BRIGHT_BLUE": "\033[94m",
"BRIGHT_MAGENTA": "\033[95m",
"BRIGHT_CYAN": "\033[96m",
"BRIGHT_WHITE": "\033[97m",
}
# Card constants
SUITS = {"Hearts": "♥", "Diamonds": "♦", "Clubs": "♣", "Spades": "♠"}
RANKS = {"2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "10": 10, "J": 11, "Q": 12, "K": 13, "A": 14}
def create_deck():
"""Creates the initial Dungeon deck."""
deck = []
for suit_name, suit_symbol in SUITS.items():
for rank_name, rank_value in RANKS.items():
is_red = suit_name in ["Hearts", "Diamonds"]
is_face_or_ace = rank_name in ["J", "Q", "K", "A"]
if is_red and is_face_or_ace:
continue
card_type = ""
if suit_name in ["Clubs", "Spades"]:
card_type = "Monster"
card_color = COLORS["RED"]
elif suit_name == "Diamonds":
card_type = "Weapon"
card_color = COLORS["YELLOW"]
elif suit_name == "Hearts":
card_type = "Health Potion"
card_color = COLORS["GREEN"]
deck.append({
"name": f"{rank_name}{suit_symbol}",
"suit": suit_name,
"rank": rank_name,
"value": rank_value,
"type": card_type,
"color": card_color
})
return deck
def clear_screen():
"""Clears the terminal screen."""
os.system('cls' if os.name == 'nt' else 'clear')
def render_game_state(player_health, equipped_weapon, last_monster_value, room_cards, dungeon_deck_size, turns, message="", can_skip=False, choices_made=0):
"""Renders the entire game interface."""
clear_screen()
print("========================================")
print(" SCOUNDREL")
print("========================================")
print(f" Health: {COLORS["GREEN"]}{player_health}/20{COLORS["RESET"]} Turn: {turns}")
weapon_name = f"{COLORS["YELLOW"]}Barehanded{COLORS["RESET"]}"
weapon_info = ""
if equipped_weapon:
weapon_name = f"{COLORS["YELLOW"]}{equipped_weapon['name']} ({equipped_weapon['value']}){COLORS["RESET"]}"
if last_monster_value != float('inf'):
weapon_info = f" (vs monsters < {last_monster_value})"
else:
weapon_info = " (newly equipped)"
print(f" Weapon: {weapon_name}{weapon_info}")
print(f" Deck: {dungeon_deck_size} cards remaining")
print("----------------------------------------")
if message:
sentences = re.split(r'(?<=[.!?])\s+', message)
for sentence in sentences:
if sentence: # Avoid printing empty strings
print(f" {sentence.strip()}")
print("----------------------------------------")
print(" Room:")
if room_cards:
for i, card in enumerate(room_cards):
card_color = ""
if card['type'] == 'Monster':
card_color = COLORS["RED"]
elif card['type'] == 'Weapon':
card_color = COLORS["YELLOW"]
elif card['type'] == 'Health Potion':
card_color = COLORS["GREEN"]
print(f" {i + 1}: {card_color}{card['name']}{COLORS["RESET"]} ({card['type']})")
else:
print(" The room is empty.")
print("========================================")
def game():
"""The main function for the Scoundrel game."""
clear_screen()
print("Welcome to Scoundrel!")
player_health = 20
dungeon_deck = create_deck()
random.shuffle(dungeon_deck)
equipped_weapon = None
last_monster_value = float('inf')
carry_over_card = None
turns = 0
can_skip = True
message = ""
# The main game loop
while player_health > 0 and (len(dungeon_deck) > 0 or carry_over_card):
turns += 1
room_cards = []
if carry_over_card:
room_cards.append(carry_over_card)
carry_over_card = None
while len(room_cards) < 4 and len(dungeon_deck) > 0:
room_cards.append(dungeon_deck.pop(0))
if not room_cards:
break
potions_used_this_turn = 0
choices_made = 0
# Player's turn loop (3 choices)
for i in range(3):
if not room_cards or player_health <= 0:
break
render_game_state(player_health, equipped_weapon, last_monster_value, room_cards, len(dungeon_deck), turns, message, can_skip, choices_made)
message = "" # Clear message after displaying it once
# Get player choice
valid_choices = [str(j + 1) for j in range(len(room_cards))]
prompt = f"Choose a card (1-{len(room_cards)})"
if can_skip and choices_made == 0:
prompt += ", 's' to skip"
prompt += " or 'q' to quit"
valid_choices.append('q')
if can_skip and choices_made == 0:
valid_choices.append('s')
player_input = ""
while player_input not in valid_choices:
player_input = input(f" {prompt}: ").lower()
if player_input not in valid_choices:
message = "Invalid choice. Please try again."
render_game_state(player_health, equipped_weapon, last_monster_value, room_cards, len(dungeon_deck), turns, message, can_skip, choices_made)
# Handle quitting the game
if player_input == 'q':
player_health = 0 # Set health to 0 to end the game loop
message = "You quit the game."
break
# Handle skipping the room
if player_input == 's':
message = "You skipped the room. The cards are returned to the deck."
dungeon_deck.extend(room_cards)
random.shuffle(dungeon_deck) # Shuffle them back in
room_cards = []
can_skip = False
break
choices_made += 1
choice = int(player_input)
chosen_card = room_cards.pop(choice - 1)
# --- Resolve the chosen card ---
if chosen_card['type'] == 'Monster':
damage = 0
if equipped_weapon and chosen_card['value'] < last_monster_value:
damage = max(0, chosen_card['value'] - equipped_weapon['value'])
message = f"You fought {COLORS["RED"]}{chosen_card['name']}{COLORS["RESET"]} with your {COLORS["YELLOW"]}{equipped_weapon['name']}{COLORS["RESET"]}. You took {COLORS["RED"]}{damage}{COLORS["RESET"]} damage."
last_monster_value = chosen_card['value']
else:
damage = chosen_card['value']
message = f"You fought {COLORS["RED"]}{chosen_card['name']}{COLORS["RESET"]} barehanded and took {COLORS["RED"]}{damage}{COLORS["RESET"]} damage."
player_health -= damage
elif chosen_card['type'] == 'Weapon':
if equipped_weapon:
message = f"You discarded your {COLORS["YELLOW"]}{equipped_weapon['name']}{COLORS["RESET"]}. "
equipped_weapon = chosen_card
last_monster_value = float('inf')
message += f"You equipped the {COLORS["YELLOW"]}{equipped_weapon['name']}{COLORS["RESET"]}."
elif chosen_card['type'] == 'Health Potion':
if potions_used_this_turn == 0:
heal_amount = chosen_card['value']
player_health = min(20, player_health + heal_amount)
potions_used_this_turn += 1
message = f"You drank a potion and restored {COLORS["GREEN"]}{heal_amount}{COLORS["RESET"]} health."
else:
message = "You can only use one potion per room. It's discarded."
if player_health <= 0:
break
# After the player's turn
if player_input != 's':
can_skip = True
if room_cards:
carry_over_card = room_cards[0]
if message: # if there's a message from the last action, append to it
message += f" {carry_over_card['name']} is carried to the next room."
else:
message = f"{carry_over_card['name']} is carried to the next room."
# --- Game Over ---
clear_screen()
print("========================================")
print(f"{COLORS["BRIGHT_RED"]} GAME OVER{COLORS["RESET"]}")
print("========================================")
if player_health <= 0:
if message == "You quit the game.":
print(f"{COLORS["YELLOW"]} You quit the game.{COLORS["RESET"]}")
else:
print(f"{COLORS["RED"]} You have been defeated!{COLORS["RESET"]}")
else:
print(f"{COLORS["GREEN"]} Congratulations! You cleared the dungeon!{COLORS["RESET"]}")
print(f" Total Turns: {turns}")
print("========================================")
def show_intro_screen():
"""Displays the intro screen with ASCII art."""
clear_screen()
print(f"{COLORS["RED"]} SCOUNDREL{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]}========================================={COLORS["RESET"]}")
print(f"{COLORS["WHITE"]} A card game of dungeon crawling{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]}========================================={COLORS["RESET"]}")
print(f"{COLORS["WHITE"]} by {COLORS["BLUE"]}Zach Gage{COLORS["WHITE"]} and {COLORS["BLUE"]}Kurt Bieg{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]} Game version by {COLORS["BLUE"]}Nick Gauthier{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]}========================================={COLORS["RESET"]}")
# print(f"{COLORS["BRIGHT_WHITE"]} Press Enter to start...{COLORS["RESET"]}")
input(f"{COLORS["BRIGHT_WHITE"]} Press Enter to start...{COLORS["RESET"]}")
def show_rules_screen():
"""Displays the rules of the game."""
clear_screen()
print(f"{COLORS["BRIGHT_CYAN"]}========================================{COLORS["RESET"]}")
print(f"{COLORS["CYAN"]} RULES{COLORS["RESET"]}")
print(f"{COLORS["BRIGHT_CYAN"]}========================================{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]}Goal: Clear the dungeon deck without dying.{COLORS["RESET"]}")
print(f"""
{COLORS["WHITE"]}How to Play:{COLORS["RESET"]}""")
print(f"{COLORS["WHITE"]}1. Each turn, you get a new room of 4 cards.{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]}2. Choose 3 cards to play. The last one stays.{COLORS["RESET"]}")
print(f"""
{COLORS["WHITE"]}Card Types:{COLORS["RESET"]}""")
print(f"{COLORS["RED"]}♠ Monsters: Deal damage equal to their value.{COLORS["RESET"]}")
print(f"{COLORS["GREEN"]}♥ Potions: Heal you. Use one per room.{COLORS["RESET"]}")
print(f"{COLORS["YELLOW"]}♦ Weapons: Fight monsters with less damage.{COLORS["RESET"]}")
print(f"""
{COLORS["WHITE"]}Important:{COLORS["RESET"]}""")
print(f"{COLORS["WHITE"]}- You can skip a room, but not two in a row.{COLORS["RESET"]}")
print(f"{COLORS["WHITE"]}- An equipped weapon can only be used on monsters with a strictly lower value than the one just defeated.{COLORS["RESET"]}")
print(f"{COLORS["BRIGHT_CYAN"]}========================================{COLORS["RESET"]}")
input(f"{COLORS["BRIGHT_WHITE"]} Press Enter to continue...{COLORS["RESET"]}")
def main():
"""The main function for the Scoundrel game."""
# Temporarily bypass intro and rules for debugging
show_intro_screen()
show_rules_screen()
while True:
game()
play_again = input("Play again? (y/n):").lower()
if play_again != 'y':
break
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment