Skip to content

Instantly share code, notes, and snippets.

@Winslett
Last active April 16, 2025 12:11
Show Gist options
  • Save Winslett/a0167f1eee56c186a1b17c954de701d8 to your computer and use it in GitHub Desktop.
Save Winslett/a0167f1eee56c186a1b17c954de701d8 to your computer and use it in GitHub Desktop.
test
#!/usr/bin/env python3
import random
import numpy as np
import time
from tabulate import tabulate
import csv
from itertools import chain
class Card:
def __init__(self, name, cost_value, space_value, available):
self.name = name
self.cost_value = cost_value
self.space_value = space_value
self.initial_available = available
self.available = available
def reset_availability(self):
"""Resets the card's availability to its initial count."""
self.available = self.initial_available
def __update_availability__(self, num_bought):
self.available += (-1 * num_bought)
def __str__(self):
return f'{self.name} (Cost:{self.cost_value})'
class ActionCard(Card):
def __init__(self, name, money_value, cost_value, victory_points, space_value, available, actions=0, extra_cards=0, buys=0, is_reaction=False):
super().__init__(name, cost_value, space_value, available)
self.money_value = money_value # Number of coins it provides per use
self.victory_points = victory_points # Number of victory points it is worth at the end of the game
self.actions = actions # Number of extra actions it grants
self.extra_cards = extra_cards # Number of extra cards it grants when played
self.buys = buys
self.is_reaction = is_reaction
def play_action(self,game, player):
""" Method to apply the effect of the action card to a player. For example, it can increase the player's actions and draw extra cards. """
if player.actions > 0:
player.actions -= 1
player.actions += self.actions
player.draw_cards(self.extra_cards)
player.buys += self.buys
player.coins += self.money_value
def __str__(self):
return f"{self.name} (Cost: {self.cost_value}, Actions: {self.actions}, Cards: {self.extra_cards})"
class Cellar(ActionCard):
def __init__(self, name, money_value, cost_value, victory_points, space_value, available, actions=0, extra_cards=0, buys=0):
super().__init__(name, money_value, cost_value, victory_points, space_value, available, actions, extra_cards, buys)
def play_action(self,game, player):
""" Override to apply the unique effect of the Cellar card to a player, including discarding based on card utility and future benefits. """
if player.actions > 0:
player.actions -= 1
# Calculate the average coin value of the player's deck
average_coin_value = self.calculate_average_coin_value(player)
# Calculate the potential value of extra cards drawn from the action card
potential_extra_cards_value = self.calculate_potential_extra_cards_value(player)
# Filter the cards in hand to find those with lower coin value than the average, but consider extra card benefits
cards_to_discard = []
for card in player.hand:
# If the card has a lower value than the deck average and won't complement future actions, discard it
if card.money_value < average_coin_value and (card.money_value < potential_extra_cards_value):
cards_to_discard.append(card)
# Discard the least valuable cards (up to a number determined by the player)
for card in cards_to_discard:
player.discard_card(card)
self.extra_cards+=1
# Draw the extra cards as usual
player.draw_cards(self.extra_cards)
player.buys += self.buys
player.coins += self.money_value
def calculate_average_coin_value(self, player):
""" Calculate the average coin value of the player's deck, considering all cards in hand, deck, and discard pile. """
total_coin_value = 0
total_cards = 0
# Include the player's deck, discard pile, and hand for the calculation
all_cards = player.hand + player.deck + player.discard_pile
for card in all_cards:
total_coin_value += card.money_value
total_cards += 1
return total_coin_value / total_cards if total_cards > 0 else 0 # Avoid division by zero
def calculate_potential_extra_cards_value(self, player):
""" Estimate the potential value of extra cards that may be drawn after playing the action card. """
total_coin_value = 0
total_cards = 0
# Simulate the value of the extra cards based on their type
for _ in range(self.extra_cards):
total_coin_value += self.estimate_card_value(player)
total_cards += 1
return total_coin_value / total_cards if total_cards > 0 else 0 # Avoid division by zero
def estimate_card_value(self, player):
""" Estimate the value of a card drawn from the deck (can be modified to simulate better). """
return self.calculate_average_coin_value(player)
def __str__(self):
return f"{self.name} (Cost: {self.cost_value}, Actions: {self.actions}, Cards: {self.extra_cards})"
class Trasher(ActionCard):
def __init__(self, name, money_value, cost_value, victory_points, space_value, available, actions=0, extra_cards=0, buys=0, required_trashes = 0, optional_trashes = 0, trash_first = False, trash_location="hand" , trashing_effect= False):
super().__init__( name, money_value, cost_value, victory_points, space_value, available, actions=0, extra_cards=0, buys=0)
self.required_trashes = required_trashes
self.optional_trashes = optional_trashes
self.trash_first = trash_first
self.trash_location = trash_location
self.trashing_effect = trashing_effect
def play_action(self,game, player):
if self.trash_first:
print(f"{player.name} is trashing cards before gaining benefits.")
trashed = player.trash_cards(self.required_trashes, self.optional_trashes, self.trash_location )
if trashed and self.trashing_effect:
super().play_action(game,player)
print(f"{player.name} trashed {[card.name for card in trashed]}.")
if not(self.trashing_effect):
super().play_action(game,player)
if not(self.trash_first):
print(f"{player.name} is trashing cards after gaining benefits.")
trashed = player.trash_cards(self.required_trashes, self.optional_trashes, self.trash_location)
print(f"{player.name} trashed {[card.name for card in trashed]}.")
class AttackCard(ActionCard):
def __init__(self, name, money_value, cost_value, victory_points, space_value, available, actions=0, extra_cards=0, buys=0, discard_down_to=None, gives_curse=False, trash_range=None, topdeck_victory=False):
super().__init__( name, money_value, cost_value, victory_points, space_value, available, actions=0, extra_cards=0, buys=0)
self.discard_down_to = discard_down_to # If set, forces discard down to X cards
self.gives_curse = gives_curse # If True, the attack gives a Curse
self.trash_range = trash_range # If set, trashes cards in a cost range
self.topdeck_victory = topdeck_victory # If True, forces topdecking a Victory card
def play_action(self,game, player):
super().play_action(game,player)
self.attack(game, player)
def attack(self, game, attacker):
"""Apply attack effects to all opponents."""
for player in game.players:
if player != attacker and not player.has_reaction():
self.apply_attack(player, game)
def apply_attack(self, player, game):
"""Handles discard, curse-giving, trashing, and bureaucrat effects dynamically."""
if self.discard_down_to is not None:
while len(player.hand) > self.discard_down_to:
discarded_card = player.choose_discard()
player.discard_pile.append(discarded_card)
player.hand.remove(discarded_card)
print(f"{player.name} discards {discarded_card.name}")
curse_card = None #initialize curse_card
if self.gives_curse:
curse_cards = [card for card in game.supply if card.name == "Curse" and card.available > 0]
if curse_cards:
curse_card = next((card for card in game.supply if card.name == "Curse" and card.available > 0), None)
if curse_card:
player.discard_pile.append(curse_card)
curse_card.available -= 1 #reduce available amount
print(f"{player.name} gains a Curse!")
if self.trash_range is not None:
top_card = player.reveal_top()
if top_card and self.trash_range[0] <= top_card.cost <= self.trash_range[1]:
player.trash(top_card, game.trash_pile)
print(f"{player.name} trashes {top_card.name}")
else:
print(f"{player.name} reveals {top_card.name}, but it is safe.")
if self.topdeck_victory:
victory_cards = [card for card in player.hand if card.is_victory()]
if victory_cards:
chosen_card = player.choose_victory_to_topdeck(victory_cards)
player.hand.remove(chosen_card)
player.deck.insert(0, chosen_card)
print(f"{player.name} places {chosen_card.name} on top of their deck.")
else:
print(f"{player.name} reveals they have no Victory cards in hand.")
class VictoryCard(Card):
def __init__(self, name, cost_value, victory_points, space_value, available):
super().__init__(name, cost_value, space_value, available)
self.victory_points = victory_points
self.money_value = 0
def __str__(self):
return f'{self.name} (Cost: {self.cost_value}, Victory_Points: {self.victory_points})'
class TreasureCard(Card):
def __init__(self, name, money_value, cost_value, space_value, available):
super().__init__(name, cost_value, space_value, available)
self.money_value = money_value
def __str__(self):
return f"{self.name} (Cost: {self.cost_value}, Money: {self.money_value})"
class Player:
def __init__(self, name, card_names, action_card_names, buy_priority_system=None, play_action_priority_system=None):
self.name = name
self.initial_deck = [TreasureCard('Copper', 1, 0, 1, 50)] * 7 + [VictoryCard('Estate', 2, 1, 1, 8)] * 3
self.buy_priority_system = buy_priority_system if buy_priority_system else generate_random_priority(card_names)
self.wins = 0
self.losses = 0 # Initialize losses
self.ties = 0 # Initialize tiebreaker_score
self.total_VP=0
self.totalRevenue = 0
self.unusedcoins = 0
self.totalCards = []
def reset_deck(self):
"""Resets the player's deck, discard pile, and hand to the initial state."""
self.deck = self.initial_deck.copy()
random.shuffle(self.deck)
self.discard_pile = []
self.play_area = []
self.hand = []
self.actions = 1 # Default number of actions per turn
self.buys = 1
self.coins = 0
self.draw_cards(5)
def draw_cards(self, num):
"""Draw 'num' cards from the deck (simulation for this example).""" # Check if the deck has enough cards
if len(self.deck) < num:
# Shuffle the discard pile back into the deck and shuffle
self.shuffle_deck()
# Draw the cards from the deck
drawn_cards = self.deck[:num]
self.hand.extend(drawn_cards)
self.deck = self.deck[num:]
# Remove the drawn cards from the deck
print(f"{self.name} drew {num} cards.")
def shuffle_deck(self):
"""SHuffle the discard pile into the deck and shuffle the deck."""
random.shuffle(self.discard_pile)
self.deck.extend(self.discard_pile)
self.discard_pile.clear()
def play_card(self, card, game):
"""Simulate playing a card."""
print(f"{self.name} plays {card.name}")
if isinstance(card, ActionCard):
card.play_action(game, self)
def trash_cards(self, required_trashes, optional_trashes, trash_location):
"""Trashes the specified number of required and optional cards based on the player's buy priority."""
trashed_cards = []
# Determine the source of trashing
if trash_location == "hand":
source = self.hand
elif trash_location == "deck":
source = self.deck
elif trash_location == "discard":
source = self.discard_pile
else:
print(f"Invalid trash location: {trash_location}")
return []
# Ensure Estates and Coppers are in the buy priority list
adjusted_priority = self.buy_priority_system.copy()
if "Estate" not in adjusted_priority:
adjusted_priority.append("Estate")
if "Copper" not in adjusted_priority:
adjusted_priority.append("Copper")
if "Curse" not in adjusted_priority:
adjusted_priority.append("Curse")
# Sort the cards in source by their position in the buy priority list (lower index = more desirable to keep)
source.sort(key=lambda card: adjusted_priority.index(card.name) if card.name in adjusted_priority else float('inf'))
# Trash the required number of cards, starting from the least desirable
for _ in range(required_trashes):
if source:
trashed_card = source.pop(-1) # Remove from the end (least desirable)
trashed_cards.append(trashed_card)
# Trash optional cards if possible
for _ in range(optional_trashes):
if source:
trashed_card = source.pop(-1)
trashed_cards.append(trashed_card)
else:
break # Stop if there are no more cards to trash
print(f"{self.name} trashed {[card.name for card in trashed_cards]} from {trash_location}.")
return trashed_cards
def discard_card(self, card):
""" Discards a card by adding it to the discard pile. """
if card in self.hand:
self.hand.remove(card) # Remove the card from the hand
self.discard_pile.append(card) # Add the card to the discard pile
else:
print(f"Card {card.name} is not in hand to discard.")
def choose_discard(self):
# Rank cards based on their position in the player's buy priority list
card_priorities = {card: self.get_card_buy_priority(card) for card in self.hand}
# Sort cards by priority (based on their position in the buy priority list)
sorted_cards = sorted(self.hand, key=lambda card: card_priorities[card], reverse=True)
# Discard cards until we reach the target count
cards_to_discard = sorted_cards[self.discard_down_to:]
discarded_cards = []
for card in cards_to_discard:
discarded_cards.append(card)
self.hand.remove(card)
self.discard_pile.append(card)
print(f"{self.name} discards {card.name}")
return discarded_cards
def get_card_buy_priority(self, card):
"""
Determines the priority of a card based on its position in the player's buy priority list.
The position in the list corresponds to the priority.
"""
try:
# If the card is in the buy priority list, return its index (position in the list)
return self.buy_priority.index(card.name)
except ValueError:
# If the card is not in the buy priority list, give it a low priority (e.g., last)
return len(self.buy_priority)
def has_reaction(self):
"""Checks if the player has any reaction cards in their hand."""
for card in self.hand:
if isinstance(card, ActionCard) and card.is_reaction:
return True
return False
# Initializing my cards
cards = [
VictoryCard('Province', 8, 6, 1, 8),
TreasureCard('Gold', 3, 6, 1, 20),
VictoryCard('Duchy', 5, 3, 1, 8),
AttackCard(name="Witch", money_value=0, cost_value=5, victory_points=0, space_value=0, available=True, actions=0, extra_cards=2, buys=0, discard_down_to=None, gives_curse=True, trash_range=None, topdeck_victory=False),
ActionCard('Laboratory', 0, 5, 0, 1, 10, actions = 1, extra_cards = 2, buys = 0,is_reaction = False),
ActionCard('Festival', 2, 5, 0, 1, 10, actions = 2, extra_cards = 0, buys = 1,is_reaction = False),
ActionCard('Market', 1, 5, 0, 1, 10, actions = 1, extra_cards = 1, buys = 1,is_reaction = False),
Trasher("Money Lender", 3, 4, 0, 1, 10, required_trashes=1, optional_trashes=0, trash_first=True, trash_location="hand", trashing_effect=True ),
TreasureCard('Silver', 2, 3, 1, 30),
ActionCard("Moat", 0, 2, 0, 1, 10, actions = 0, extra_cards = 0, buys = 0, is_reaction = True),
Cellar(name="Cellar", money_value=0, cost_value=2, victory_points=0, space_value=1, available=10, actions=1, extra_cards=0, buys=0),
Trasher("Chapel", 0, 2, 0, 1, 10, required_trashes=0, optional_trashes=4, trash_first=True, trash_location="hand", trashing_effect=False ),
VictoryCard('Estate', 2, 1, 1, 8),
TreasureCard('Copper', 1, 0, 1, 50),
VictoryCard('Curse', 0, -1, 1, 10)
]
# Extract card names from the card objects
card_names = [card.name for card in cards]
action_card_names = [card.name for card in cards if isinstance(card, ActionCard)]
victory_card_names = [card.name for card in cards if isinstance(card, VictoryCard)]
treasure_card_names = [card.name for card in cards if isinstance(card, TreasureCard)]
def generate_random_priority(card_names):
"""Generate a random card purchasing priority list."""
priority_list = [card for card in card_names if card != "Curse"] # Exclude "Curse"
priority_list = [card for card in priority_list if random.randint(1, 4) != 1]
return priority_list
num_players = int(input('How many players are in this tournament (4-128)? : '))
players = [Player(f'Player {i+1}', card_names, action_card_names) for i in range(num_players)]
for player in players:
print(f"{player.name}'s buying priority: {player.buy_priority_system}")
cards.append(VictoryCard('Curse', 0, -1, 1, 10))
def action_phase(player,game):
"""Simulates the action phase of a player's turn, prioritizing action cards based on play_action_priority_system."""
print(f"\n{player.name}'s Action Phase begins.")
# Iterate over a COPY of player.hand to prevent modification issues
for card in player.hand[:]:
if isinstance(card, ActionCard):
player.play_card(card, game) # Play the action card
if card in player.hand: # Only remove if it wasn't trashed
player.play_area.append(card) # Move it to play area
player.hand.remove(card)
player.discard_pile.extend(player.play_area) # Move played cards to discard pile
player.play_area.clear() # Clear play area
print(f"{player.name}'s Action Phase ends.\n")
def treasure_phase(player):
"""Simulates the Treasure Phase where a player plays all Treasure Cards to generate coins."""
print(f"\n{player.name}'s Treasure Phase begins.")
# Find all Treasure Cards in the player's hand
treasure_cards = [card for card in player.hand if isinstance(card, TreasureCard)]
if not treasure_cards:
print(f"{player.name} has no Treasure Cards to play.")
else:
for card in treasure_cards:
player.coins += card.money_value
# Move all played Treasure Cards to the discard pile
player.hand = [card for card in player.hand if not isinstance(card, TreasureCard)]
player.discard_pile.extend(treasure_cards)
print(f"{player.name} has {player.coins} coins available for the Buy Phase.")
print(f"{player.name}'s Treasure Phase ends.\n")
def buy_phase(player, cards):
"""Simulates the Buy Phase where a player purchases cards based on their buy_priority_system."""
print(f"\n{player.name}'s Buy Phase begins.")
#storing hand info
player.totalRevenue+=player.coins
while player.buys > 0 and player.coins >= 0:
# Look through the player's buy priority and find an affordable, available card
for card_name in player.buy_priority_system:
# Find the card object in the main cards list
chosen_card = next((card for card in cards if card.name == card_name), None)
if chosen_card and chosen_card.cost_value <= player.coins and chosen_card.available > 0:
# Buy the card
player.coins -= chosen_card.cost_value
player.buys -= 1
player.discard_pile.append(chosen_card)
chosen_card.__update_availability__(1) # Reduce availability by 1
print(f"{player.name} buys {chosen_card.name} for {chosen_card.cost_value} coins.")
break # Move to next buy (if any buys remain)
else:
# No affordable cards found
break
print(f"{player.name} has {player.coins} coins left and {player.buys} buys remaining.")
player.unusedcoins += player.coins
print(f"{player.name}'s Buy Phase ends.\n")
def cleanup_phase(player):
"""Simulate the Clean-Up Phase where the player discards their hand and draws new cards."""
print(f"{player.name}'s Clean-Up Phase:")
# Discard all cards in hand and played cards
player.discard_pile.extend(player.hand)
player.hand.clear()
# Draw 5 new cards
player.draw_cards(5)
# Unused Actions, Buys, and Coins do not carry over
player.actions = 1
player.buys = 1
player.coins = 0
print(f"{player.name} ends their turn.")
def play_turn(player, cards, game):
"""Simulates a full turn for a given player, including Action, Treasure, and Buy phases."""
print(f"\n===== {player.name}'s Turn =====")
action_phase(player,game) # Call the Action Phase
treasure_phase(player) # Call the Treasure Phase
buy_phase(player, cards) # Call the Buy Phase
cleanup_phase(player)
print(f"===== {player.name}'s Turn Ends =====\n")
def game_over(cards):
"""Checks if the game should end based on depletion conditions."""
province_card = next((card for card in cards if card.name == "Province"), None)
# End game if Provinces are gone
if province_card and province_card.available <= 0:
print("All Provinces are gone! Game over.")
return True
# End game if 3 supply piles are empty
empty_piles = sum(1 for card in cards if card.available <= 0)
if empty_piles >= 3:
print("Three supply piles are empty! Game over.")
return True
return False
def determine_winner(player1, player2):
"""Determines the winner by counting Victory Points."""
def count_victory_points(player):
# Combine all zones: deck, hand, discard_pile, and play_area
all_cards = player.deck + player.hand + player.discard_pile + player.play_area
# Count total victory points from all VictoryCard instances
total_points = sum(card.victory_points for card in all_cards if isinstance(card, VictoryCard))
return total_points
p1_score = count_victory_points(player1)
p2_score = count_victory_points(player2)
player1.total_VP += p1_score
player2.total_VP += p2_score
print(f"{player1.name} Victory Points: {p1_score}")
print(f"{player2.name} Victory Points: {p2_score}")
if p1_score > p2_score:
return player1
elif p2_score > p1_score:
return player2
else:
return int(0)
def play_game(player1, player2, cards):
"""Simulates a full game between two players."""
print("\n===== New Game Begins =====")
turn_count = 1
max_turns = 24 # Set a reasonable limit to prevent infinite games
#create a game object
class Game:
def __init__(self, players, supply):
self.players = players
self.supply = supply
self.trash_pile = []
game = Game([player1, player2], cards)
while turn_count <= max_turns:
print(f"\n===== Turn {turn_count} =====")
# Player 1's turn
play_turn(player1, cards, game) #pass game
# Check if the game should end
if game_over(cards):
break
# Player 2's turn
play_turn(player2, cards, game) #pass game
# Check if the game should end
if game_over(cards):
break
turn_count += 1
print("\n===== Game Over! =====")
# Determine the winner
winner = determine_winner(player1, player2)
return winner
# Round-Robin Tournament function
def play_round_robin(players,cards):
"""Schedules a round-robin tournament where each player plays against every other player."""
print("\nRound-Robin Tournament Starting...\n")
# Each player plays against every other player
for i in range(len(players)):
for j in range(i + 1, len(players)):
print(f"\n{players[i].name} vs {players[j].name}")
if 1 == random.randint(1,2):
player1 = players[i]
player2 = players[j]
else:
player1 = players[j]
player2 = players[i]
# Reset players' decks
player1.reset_deck()
player2.reset_deck()
# Reset card availabilities
for card in cards:
card.reset_availability()
winner = play_game(player1, player2, cards)
if winner != 0:
if winner == players[i]:
players[j].losses +=1
if winner == players[j]:
players[i].losses +=1
winner.wins += 1
print(f"{winner.name} wins this match!\n")
else:
players[i].ties+=1
players[j].ties+=1
print('Tie')
for player in [players[i], players[j]]:
player.totalCards.extend((player.discard_pile))
player.totalCards.extend((player.deck))
def display_tournament_results(players):
headers = ["Player", "Wins", "Losses", "Ties", "Win Rate", "Total Victory Points", "Total Revenue", "Total Unused Coins", "Buy Priority System"]
headers.extend([f"{card.name} Presence" for card in cards]) # Add a column for each card in 'cards'
table_data = []
for player in players:
total_games = player.wins + getattr(player, 'losses', 0) + getattr(player, 'ties', 0)
win_rate = player.wins / total_games if total_games > 0 else 0 # Avoid division by zero
card_count = {}
total_cards = len(player.totalCards) # Total number of cards in player's deck
# Prepare percentage makeup for each card in card_count
# Count occurrences of each card in player.totalCards
for card in player.totalCards:
if hasattr(card, "name"):
card_name = card.name
card_count[card_name] = card_count.get(card_name, 0) + 1
percentage_makeup = {}
for card_name, count in card_count.items():
percentage_makeup[card_name] = (count / total_cards) * 100 # Percent presence for each card
# Debugging percentage makeup
print(f"{player.name}'s percentage makeup: {percentage_makeup}") # Debugging line
# Add row data for the current player
row = [
player.name,
player.wins,
getattr(player, 'losses', 'N/A'),
getattr(player, 'ties', 'N/A'),
f"{win_rate:.2%}", # Format win rate as percentage
getattr(player, 'total_VP', 'N/A'),
getattr(player, 'totalRevenue', 'N/A'),
getattr(player, 'unusedcoins', 'N/A'),
player.buy_priority_system,
]
# Add the percentage for each card to the row
for card in cards:
row.append(f"{percentage_makeup.get(card.name, 0):.2f}%") # Add percentage for each card
table_data.append(row)
# Sort players by win rate (descending)
table_data.sort(key=lambda row: float(row[4].strip('%')) / 100, reverse=True)
# Print table using tabulate
print(tabulate(table_data, headers=headers, tablefmt="grid"))
# Save as a CSV file
with open("tournament_results.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(headers)
writer.writerows(table_data)
print("\nResults saved to 'tournament_results.csv'")
# Determine the overall winner
winner = max(players, key=lambda p: (p.wins / (p.wins + getattr(p, 'losses', 1) + getattr(p, 'ties', 0)), p.wins))
print(f"\n{winner.name} wins the Round-Robin Tournament with {winner.wins} wins!")
play_round_robin(players,cards)
display_tournament_results(players)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment