Python can be used to simulate games. A particular use could be for testing strategies. The advantage of using a computer is that it can easily carry out a large number of trials, which can make it clear which strategy is best.
# Standard imports
import numpy as np
import matplotlib.pyplot as plt
import math as m
from mpmath import mp, iv
import random as random_number
In this notebook we simulate a game similar to Exploding Kittens, which I've been enjoying playing with my family while stuck at home during the coronavirus epidemic.
I'll describe a similar game for two players with slightly different cards. Basically, players take turns. During their turn they draw one card, and try to avoid drawing a bomb card. If they draw a bomb card they immediately lose the game unless they have a diffuse card. When a player uses a diffuse card, they insert the bomb back into the draw pile anywhere they want. Other cards can modify how the game proceeds and are played on a player's turn before they draw a card.
Here is a class representing a card in the game.
class Card:
def __init__(self, name, description):
"""Construct a new card with the provided name and description.
Both parameters should be strings."""
self._name = name
self._description = description
def name(self):
"""Return the name of the card as a string."""
return self._name
def description(self):
"""Return a description of what the card does as a string."""
return self._description
def __repr__(self):
return "Card('{}', '{}')".format(self._name, self._description)
def __str__(self):
return "a {} card".format(self._name)
We'll just instantiate one of each card type. This is done below.
# Special Cards:
bomb_card = Card("bomb",
"The player who draws this card loses unless they have a defuse card.")
defuse_card = Card("defuse",
"Having this card in your possession prevents a Bomb card from exploding.")
# Cards you can play to end your turn:
skip_card = Card("skip",
"Playing this card allows you to immediately end your turn without drawing a card.")
bottom_card = Card("bottom",
"Playing will result in you immediately drawing a card from the bottom of the deck.")
# Cards you can play anytime
shuffle_card = Card("shuffle",
"Playing this card results in the draw pile being shuffled.")
see_card = Card("see",
"Playing this card will return a tuple consisting of the top three cards in the deck.")
nothing_card = Card("nothing",
"This card does nothing. You can't even play it.")
card_list = [bomb_card, defuse_card, skip_card, bottom_card, shuffle_card, see_card, nothing_card]
for card in card_list:
print("{}: {}\n".format(card.name(), card.description()))
Players have a hand of cards in the game. They can not hold a bomb card, but can have any other card.
A hand just needs to keep track of how many of each card the player has. We can add a card to a players hand, remove a card, obtain the number of cards of a certain type the hand has, make a copy of the hand, find the total number of cards, and print the contents of a players hand. An implementation is below.
class Hand:
def __init__(self, d = None):
if d is None:
"""Initialize an empty hand."""
self._d = {}
# Special cards you can hold
self._d[defuse_card] = 0
# Cards you can play to end your turn
self._d[skip_card] = 0
self._d[bottom_card] = 0
# Cards you can play before ending your turn
self._d[shuffle_card] = 0
self._d[see_card] = 0
self._d[nothing_card] = 0
else:
self._d = d.copy()
def add_card(self, card):
self._d[card] += 1
def remove_card(self, card):
if self._d[card] > 0:
self._d[card] -= 1
else:
raise ValueError("Attempted to play a {} card, which is not in this hand!".format(card))
def num_cards(self, card):
if card in self._d:
return self._d[card]
else:
return 0
def copy(self):
return Hand(self._d)
def __len__(self):
"""Return the total number of cards."""
total = 0
for card,num in self._d.items():
total += num
return total
def __str__(self):
count = 0
s = ""
last = ""
for card,num in self._d.items():
if num > 0:
if count > 0:
s += last + ", "
count+=1
if num == 1:
last = "a {}".format(card.name())
else:
last = "{} {}s".format(num, card.name())
if count == 0:
s += "no cards"
elif count == 1:
s += last
else:
s += "and " + last
return s
Here I demonstrate using the class.
h = Hand()
h.add_card(defuse_card)
h.add_card(skip_card)
print("h is a hand holding {}.".format(h))
print("h holds {} cards.".format( len(h) ))
print("h holds {} defuse cards.".format( h.num_cards(defuse_card) ))
h2 = h.copy()
print("h2 is a hand holding {}.".format(h2))
h2.remove_card(defuse_card)
print("After removing a defuse card, h2 is a hand holding {}. h is holding {}.".format(h2, h))
The GameData class will represent the current state of the game from a player's viewpoint.
Assuming gd
is a GameData object passed to the player, the player can access:
gd.my_hand()
: their handgd.draw_pile_size()
: the number of cards in the draw pilegd.other_players_hand_count()
: the number of cards in the other player's handgd.discard_pile()
: a tuple containing the cards played in order from first to last played.Calling str(gd)
will return a human readable description of the game data.
class GameData:
def __init__(self,
draw_pile_size, # The number of cards remaining in the draw_pile
discard_pile, # List of cards, from the first card
# played (at index 0) to last.
hand, # the current player's hand.
other_players_hand_count, # number of cards opponent has
):
self._draw_pile_size = draw_pile_size
self._discard_pile = tuple(discard_pile)
self._hand = hand.copy()
self._other_players_hand_count = other_players_hand_count
def _update(self, hand, discard_pile):
# When a card is played, both the hand and the discard pile should be updated.
self._hand = hand.copy()
self._discard_pile = tuple(discard_pile)
def my_hand(self):
"""Return the active player's hand."""
return self._hand
def draw_pile_size(self):
return self._draw_pile_size
def other_players_hand_count(self):
"""return the number of cards in the other player's hand."""
return self._other_players_hand_count
def discard_pile(self):
"""Return a tuple containing the discard pile.
The last placed card has the greatest index."""
return self._discard_pile
def __str__(self):
s = "GAME DATA: "
if self.draw_pile_size() == 1:
s += "one card remains in the draw pile.\n"
else:
s += "{} cards remain in the draw pile.\n".format(self.draw_pile_size())
s += " Your hand holds {}.\n".format(str(self._hand))
s += " The other player holds {} cards.\n".format(
self._other_players_hand_count)
if len(self.discard_pile()) == 0:
s += " The discard pile is empty."
else:
if len(self.discard_pile()) == 1:
top_card = self.discard_pile()[-1]
s += " The only card in the discard pile is {}.".format(top_card.name())
elif len(self.discard_pile()) == 2:
top_card = self.discard_pile()[-1]
bottom_card = self.discard_pile()[-2]
s += " The top card in the discard pile is {} ".format(top_card.name()) + \
"and the only other card is {}.".format(bottom_card.name())
else:
s += " The discard pile has {} cards.\n".format(len(self.discard_pile()))
top_card = self.discard_pile()[-1]
card2 = self.discard_pile()[-2]
card3 = self.discard_pile()[-3]
s += " The top three cards are {}, {} and {}." \
.format(top_card.name(), card2.name(), card3.name())
return s
Here is a brief demonstration of the class.
h = Hand()
h.add_card(defuse_card)
h.add_card(shuffle_card)
discard_pile=[nothing_card, see_card]
gd = GameData(10, # The number of cards remaining in the draw_pile
discard_pile, # List of cards, from the first card
# played (at index 0) to last.
h, # the current player's hand.
3 # number of cards opponent has
)
print(gd)
Now imagining that the player plays a shuffle card, we'd update the data.
h.remove_card(shuffle_card)
discard_pile.append(shuffle_card)
gd._update(h, discard_pile)
print(gd)
We want to write an abstract player class that represents all the choices a player has to make. The player will also have a name.
The turn
method:
In the game, during a players turn they can play one or more cards. Some cards (skip and bottom) end a player's turn immediately, and others (shuffle, see and nothing) do not. Defuse cards will be played automatically when a player draws a bomb card.
When it is a player's turn we will call the turn
method. The player will be passed:
game_data
object which represents the current state of the game from the players point of view, andplay_a_card
function which is for playing a card that does not end a players turn. For example, if in the turn
method play_a_card(see_card)
is played, then a tuple containing the identities of the top three cards in the draw pile will be returned. (If there are fewer than $3$ cards in the draw pile, then a shorter tuple is returned.) Calling play_a_card(shuffle_card)
will shuffle the draw pile but return nothing.When a player is done playing cards that do not end their turn, the player should call return
.
return
call) will result in drawing a card from the top of the deck.return skip_card
) will result in a skip card being played. The player's turn immediately ends without drawing a card.return bottom_card
) will end the players turn and place the bottom card in the players hand.The defuse
method:
If a player draws a bomb card, and has a defuse card, then we automatically play that card. The player then inserts the card back into the draw pile. The purpose of the defuse
method is to choose where to insert the card. The defuse
method will be passed game_data
like the turn
method.
The function must return an integer in the interval $[0, nc]$ where nc
is the number of card in the draw pile. The number of cards in the deck can be determined by calling game_data.draw_pile_size()
.
Calling return i
will insert the bomb card into the draw pile in such a way so that the card appears at index i
, and preserve the order of the other cards. A return value of 0
will place the card on the top of the draw pile (so that it is the next card drawn) and a return value of nc will place the card on the bottom of the draw pile.
class Player:
def name(self):
"""Return the name of this player as a string. This must be constant."""
raise NotImplementedError("name() has not been implemented yet")
def turn(self, game_data, play_a_card):
"""
Method representing a player taking a turn.
In the game, during a players turn they can play one or more cards.
Some cards (skip and bottom) end a player's turn immediately, and
others (shuffle, see and nothing) do not. Defuse cards will
be played automatically when a player draws a bomb card.
When it is a player's turn we will call the `turn` method. The
player will be passed:
* a `game_data` object which represents the current state of the
game from the players point of view, and
* a `play_a_card` function which is for playing a card *that does
not end a players turn*. For example, if in the `turn` method
`play_a_card(see_card)` is played, then a tuple containing
the identities of the top three cards in the draw pile will
be returned. (If there are fewer than $3$ cards in the draw
pile, then a shorter tuple is returned.) Calling
`play_a_card(shuffle_card)` will shuffle the draw pile but
return nothing.
When a player is done playing cards that do not end their turn, the
player should call `return`.
* Returning nothing (with a simple `return` call) will result in
drawing a card from the top of the deck.
* Returning a skip card (`return skip_card`) will result in a skip
card being played. The player's turn immediately ends without
drawing a card.
* Returning a bottom card (`return bottom_card`) will end the players
turn and place the bottom card in the players hand.
"""
raise NotImplementedError("turn() has not been implemented yet")
def defuse(self, game_data):
"""Called when the player draws a bomb card and has a defuse card.
If a player draws a bomb card, and has a defuse card, then we
automatically play that card. The player then inserts the card
back into the draw pile. The purpose of the `defuse` method is to
choose where to insert the card. The `defuse` method will be passed
`game_data` like the `turn` method.
The function must return an integer in the interval $[0, nc]$ where
`nc` is the number of card in the draw pile. The number of cards in
the deck can be determined by calling `game_data.draw_pile_size()`.
Calling `return i` will insert the bomb card into the draw pile in
such a way so that the card appears at index `i`, and preserve the
order of the other cards. A return value of `0` will place the card
on the top of the draw pile (so that it is the next card drawn) and
a return value of nc will place the card on the bottom of the draw pile.
"""
raise NotImplementedError("defuse() has not been implemented yet")
An implementation of the Player
class represents a strategy for playing the game. Below we represent a very simple strategy:
class SimplePlayer(Player):
def name(self):
"""Return the name of this player as a string. This must be constant."""
return "Simple"
def turn(self, game_data, play_a_card):
# Simply draw a card
return
def defuse(self, game_data):
# Place the card in a random position in the deck
nc = game_data.draw_pile_size()
pos = random_number.randint(0,nc)
return pos
Here is a slightly better strategy:
An implementation is below.
class ScaredPlayer(Player):
def __init__(self):
self._scared = False
self._num_defuse = 0 # keep track of the number of diffuse
# cards in the discard pile
def name(self):
"""Return the name of this player as a string. This must be constant."""
return "Scared"
def turn(self, gd, play_a_card):
# Count the number of defuse cards in the discard pile
count = 0
for card in gd.discard_pile():
if card == defuse_card:
count += 1
if count > self._num_defuse:
# The other player must have played a defuse card.
# They may have put it in the top of the deck!
self._num_defuse = count
self._scared = True
# We behave differently if we are scared or not.
if not self._scared:
if gd.my_hand().num_cards(defuse_card) > 0:
# just draw a card
return
else:
# No defuse cards. If we have a "see card", play it.
if gd.my_hand().num_cards(see_card) > 0:
top_cards = play_a_card(see_card)
if bomb_card in top_cards:
# We're scared!
self._scared = True
if bomb_card != top_cards[0]:
# The top card is safe
return
# If we reach this point, then we are scared.
# If we have a shuffle card, we'll play it, become unscared and
# take a top card.
if gd.my_hand().num_cards(shuffle_card) > 0:
play_a_card(shuffle_card)
self._scared = False
return
# If we have a skip card, we'll use it.
if gd.my_hand().num_cards(skip_card) > 0:
return skip_card
# If we have a bottom card, we'll use it.
if gd.my_hand().num_cards(bottom_card) > 0:
return bottom_card
# With no other options left, we'll just take the top card.
return
def defuse(self, game_data):
# We're scared!
self._scared = True
# Increase the defuse card count
self._num_defuse += 1
# Place the card on top of the deck
return 0
Our play_game
function takes two inputs, the two players: player0
and player1
.
There are also two options which by default are true:
print_details
: will print out what is happening in the game if true.raise_exceptions
: Sometimes a player might behave incorrectly. (E.g., turn
returns a shuffle card, or a player plays a card they don't have.) In this case we can either declare the other player the winner, or raise an exception. If this is set to True
then we raise exceptions. Exceptions are useful for debugging players.We'd probably want to disable both these options if we are playing a lot of games to compute statistics.
Start up:
We have two players. We put them in a list for easy access as players[0]
and players[1]
.
We create a deck consisting of 4 skips, 4 bottoms, 4 shuffles, 8 sees, and 4 nothings. We shuffle this deck and deal 5 cards to each player. We also give each player a defuse card. Then we add two defuse cards to the deck and one bomb card, and reshuffle. This is our draw pile.
We start with an empty discard pile and set the active player to 0. We also define a function play_a_card
for handling when the active player plays a card.
Now the game can begin. We have a loop. Each run of the loop represents a players turn. The current player index is active
and we use other
for the index of the other player.
In the main game loop, we first assemble the game data for passing to the active player. Then we call
ret = players[active].turn(game_data, play_a_card)
When the player plays a card that does not end their turn, they will use the play_a_card
function. The return value will be either None
, bottom_card
, or skip_card
respectively representing that they end their turn drawing a card from the top, bottom, or skip their turn. We then process these possibilities, updating the draw pile if necessary. When a player draws a card, they could draw a bomb card. In this case, they lose if they don't have a defuse card, and if they do have one, we call
position = players[active].defuse(game_data)
and then we insert the bomb card back into the deck at this position.
Whenever a player plays a card, we need to ensure the card can be played in the way the player does (e.g., shuffle cards can not be returned), ensure that the player has the card, then remove the card from the players hand, add the card to the discard pile. In the case of playing a card that does not end the players turn, we also need to update the game data.
We'll omit the details of how exactly cards being played are handled.
At the end of the loop we switch players and restart with the new active player.
def play_game(player0, player1, print_details = True, raise_exceptions = True):
"""
Play a game between `player0` and `player1`.
There are two options which by default are true:
* `print_details`: will print out what is happening in the game if True.
* `raise_exceptions`: Sometimes a player might behave incorrectly. (E.g.,
`turn` returns a shuffle card, or a player plays a card they don't have.)
In this case we can either declare the other player the winner, or raise
an exception. The exception are useful for debugging.
The function returns 0 if `player0` wins and returns 1 if `player1` wins.
"""
players = []
for player in [player0, player1]:
assert isinstance(player, Player)
players.append(player)
# Get the names from the players.
player_names = [player.name() for player in players]
# If the names are the same, we distinguish them with a 0 and a 1.
if player_names[0] == player_names[1]:
player_names[0] += "0"
player_names[1] += "1"
# Deal the hands and create the draw pile
draw_pile = 4*[skip_card] + 4*[bottom_card] + 4*[shuffle_card] + \
8*[see_card] + 4*[nothing_card]
random_number.shuffle(draw_pile)
hands = []
for p in [0,1]:
h = Hand()
# Add 5 cards to the hand
for i in range(5):
h.add_card(draw_pile.pop())
# Add a defuse card
h.add_card(defuse_card)
hands.append(h)
draw_pile.append(defuse_card)
draw_pile.append(defuse_card)
draw_pile.append(bomb_card)
random_number.shuffle(draw_pile)
discard = [] # The discard pile
active = 0 # Index of the active player
# This is the function we use to allow the active player to play a card.
def play_a_card(card):
if print_details:
print(" ⁃ {} played a {} card.".format(player_names[active], card.name()))
if not card in [shuffle_card, see_card]:
raise ValueError("A {} card can not be played now.".format(str(card)))
hands[active].remove_card(card)
discard.append(card)
game_data._update(hands[active], discard)
if card == shuffle_card:
# Shuffle the draw pile
random_number.shuffle(draw_pile)
if print_details:
print(" ⁃ The draw pile was shuffled.")
return
else:
# The card is a see card. We return 3 cards or fewer if necessary.
number_of_cards_to_show = min(3,len(draw_pile))
# The top of the draw_pile is the card with the largest index.
next_cards = tuple(draw_pile[-i] for i in range(1,1+number_of_cards_to_show))
return next_cards
while True:
if print_details:
print("\n• It is now {}'s turn.".format(player_names[active]))
other = (active + 1) % 2 # index of the other player.
# Assemble game data for active player
game_data = GameData(
len(draw_pile), # Number of cards in the draw pile
discard, # The discard pile
hands[active], # The active player's hand
len(hands[other])# Number of cards in the other players hand
)
try:
# The player takes his/her turn
ret = players[active].turn(game_data, play_a_card)
except Exception as e:
if print_details:
print(" ⁃ {} generated an exception on their turn: {}." \
.format(player_names[active], repr(e)))
print(" ⁃ {} wins!!!".format(player_names[other]))
if raise_exceptions:
raise e
return other
if not ret in [None, bottom_card, skip_card]:
if print_details:
print(" ⁃ {} cheated and therefore loses.".format(player_names[active]))
print(" ⁃ {} wins!!!".format(player_names[other]))
if raise_exceptions:
raise ValueError("The only valid returns from turn() are skip_card, bottom_card, and None.")
return other
if ret is skip_card:
if print_details:
print(" ⁃ {} played a {} card.".format(player_names[active], ret.name()))
hands[active].remove_card(ret)
discard.append(ret)
else:
# We draw a card either from the bottom or the top.
if ret is bottom_card:
# Draw from the bottom
if print_details:
print(" ⁃ {} played a {} card.".format(player_names[active], ret.name()))
hands[active].remove_card(ret)
discard.append(ret)
card = draw_pile.pop(0) # Take card from the bottom
if print_details:
print(" ⁃ {} drew a card from the bottom of the draw pile." \
.format(player_names[active]))
else:
assert ret is None
card = draw_pile.pop() # Draw from the top
if print_details:
print(" ⁃ {} drew a card from the top of the draw pile." \
.format(player_names[active]))
# In either case, card now stores the card drawn.
if card != bomb_card:
# Picked up some other card.
# Add it to the active player's hand
hands[active].add_card(card)
else:
# Picked up a bomb card
if print_details:
print(" ⁃ {} picked up a bomb card!".format(player_names[active]))
if hands[active].num_cards(defuse_card) <= 0:
# The player has no defuse cards. They lose!
if print_details:
print(" ⁃ {} blew up!!!".format(player_names[active]))
print(" ⁃ {} wins!!!".format(player_names[other]))
return other
else:
# Play a defuse card
if print_details:
print(" ⁃ {} plays a defuse card.".format(player_names[active]))
hands[active].remove_card(defuse_card)
discard.append(defuse_card)
# Assemble game data for active player
game_data = GameData(
len(draw_pile), # Number of cards in the draw pile
discard, # The discard pile
hands[active], # The active player's hand
len(hands[other])# Number of cards in the other players hand
)
position = players[active].defuse(game_data)
if type(position) != int or position<0 or position>len(draw_pile):
if print_details:
print(" ⁃ {} cheated and therefore loses.".format(player_names[active]))
print(" ⁃ {} wins!!!".format(player_names[other]))
if raise_exceptions:
raise ValueError("Invalid return value from defuse() " +
"in player {}.".format(player_names[active]))
draw_pile.insert(len(draw_pile)-position, bomb_card)
if print_details:
print(" ⁃ {} inserted the bomb card back in the draw pile.".format(player_names[active]))
# Switch turns
active = (active + 1) % 2
Here is an example of a game played between two SimplePlayer
s.
player0 = SimplePlayer()
player1 = SimplePlayer()
play_game(player0, player1)
play_game(SimplePlayer(), ScaredPlayer())
Play 10000 games between SimplePlayer
and ScaredPlayer
and see who does better.
N = 100000
wins = [0, 0]
for i in range(N):
winner = play_game(SimplePlayer(), ScaredPlayer(), print_details=False)
wins[winner] += 1
print("SimplePlayer won {}%, and ScaredPlayer won {}%." \
.format(wins[0]*100/N, wins[1]*100/N))
Here is a Player
that asks for input whenever a choice is needed. It also prints out game data so that the user can make informed choices.
class HumanPlayer(Player):
def __init__(self):
self._discard_pile_size = 0
def name(self):
"""Return the name of this player as a string. This must be constant."""
return "Human"
def turn(self, gd, play_a_card):
# Print the game data so that the human knows their hand.
print(gd)
if len(gd.discard_pile()) > self._discard_pile_size:
print("Last round your opponent played: ", end="")
print(gd.discard_pile()[self._discard_pile_size].name(), end="")
for i in range(self._discard_pile_size+1, len(gd.discard_pile())):
print(", " + gd.discard_pile()[i].name(), end="")
print(".")
while True:
# Ask what card to play (if any).
s = input("You may draw a card (enter) or play a card. " +
"If you have the card, you may play a shuffle (h), " +
"see (s), skip (k), or bottom (b).")
if len(s) == 0:
print("You draw a card.")
self._discard_pile_size = len(gd.discard_pile())
return
elif len(s) > 0 and (s[0]=='h' or s[0]=='H'):
if gd.my_hand().num_cards(shuffle_card) > 0:
print("You play a shuffle card.")
play_a_card(shuffle_card)
else:
print("You don't have a shuffle card.")
elif len(s) > 0 and (s[0]=='s' or s[0]=='S'):
if gd.my_hand().num_cards(see_card) > 0:
print("You play a see card.")
next_cards = play_a_card(see_card)
if len(next_cards) == 1:
print("The only card remaining is a {} card." \
.format(next_cards[0]))
elif len(next_cards) == 2:
print("The top card is {} and the only other card is {}." \
.format(next_cards[0], next_cards[1]))
else:
print("The top card is {}, ".format(next_cards[0]) +
"the second card is {}, ".format(next_cards[1]) +
"and the third card is {}.".format(next_cards[2]))
else:
print("You don't have a see card.")
elif len(s) > 0 and (s[0]=='k' or s[0]=='K'):
if gd.my_hand().num_cards(skip_card) > 0:
print("You play a skip card.")
self._discard_pile_size = len(gd.discard_pile()) + 1
return skip_card
else:
print("You don't have a skip card.")
elif len(s) > 0 and (s[0]=='b' or s[0]=='B'):
if gd.my_hand().num_cards(bottom_card) > 0:
print("You play a bottom card.")
self._discard_pile_size = len(gd.discard_pile()) + 1
return bottom_card
else:
print("You don't have a bottom card.")
def defuse(self, gd):
# Update the size of the discard pile to account for the defuse card:
self._discard_pile_size = len(gd.discard_pile())
# Print the game data for the user
print(gd)
nc = gd.draw_pile_size()
while True:
s = input("Where do you want to place the bomb card? You may choose any " +
"integer in the interval [0, {}].".format(nc) +
" Zero corresponds to the top of the deck and {} to the bottom." \
.format(nc))
try:
pos = int(s)
if 0 <= pos <= nc:
return pos
except ValueError:
print("Input was not an integer in [0, {}].".format(nc))
pass
# Place the card in a random position in the deck
print(gd)
pos = random_number.randint(0,nc)
return pos
Here we demonstrate its use.
play_game(ScaredPlayer(), HumanPlayer())
Here is another Player implementation that uses a slightly different strategy.
class MediumPlayer(Player):
def __init__(self):
# Keep track of the number of defuse cards played.
self._num_defuse = 0
def name(self):
"""Return the name of this player as a string. This must be constant."""
return "Medium"
def turn(self, game_data, play_a_card):
# print(game_data)
hand = game_data.my_hand()
# count the defuse cards in the discard pile
count = 0
for card in game_data.discard_pile():
if card == defuse_card:
count += 1
if count > self._num_defuse:
# The other player played a defuse card.
# Shuffle if possible.
if hand.num_cards(shuffle_card) > 0:
play_a_card(shuffle_card)
# Update the hand
hand = game_data.my_hand()
# update our count
self._num_defuse = count
# if we have a defuse card, just draw a card.
if hand.num_cards(defuse_card) > 0:
# Just draw a card
return
# At this point we have no defuse card.
# If there is only one card left:
if game_data.draw_pile_size() == 1:
# Attempt to play skips:
if hand.num_cards(skip_card) > 0:
return skip_card
# Other cards won't help so we'll just take a bomb:
return
# If we have a see card, use it
while hand.num_cards(see_card) > 0:
# Play a see card
next_cards = play_a_card(see_card)
# Update the hand
hand = game_data.my_hand()
if next_cards[0] == bomb_card:
# Do all we can to avoid the bomb card
if hand.num_cards(bottom_card) > 0:
return bottom_card
if hand.num_cards(skip_card) > 0:
return skip_card
if hand.num_cards(shuffle_card) > 0:
play_a_card(shuffle_card)
# Update the hand
hand = game_data.my_hand()
# Let the loop continue to play see cards
else:
# Next card is not a bomb. Just take it:
return
# Now we have no defuse cards and no see cards. Flying blind.
# If we have a shuffle card, use it and take a card.
if hand.num_cards(shuffle_card) > 0:
play_a_card(shuffle_card)
# Update the hand
hand = game_data.my_hand()
# Take a card
return
# If we have a skip card, play it.
if hand.num_cards(skip_card) > 0:
return skip_card
# If we have a bottom card, flip a coin and use it if heads.
if hand.num_cards(bottom_card) > 0:
if random_number.random() > 0.5:
# use the bottom card
return bottom_card
else:
# Just take the top card
return
# Take the top card
return
def defuse(self, game_data):
self._num_defuse += 1
# Place the card in a random position in the deck
nc = game_data.draw_pile_size()
hand = game_data.my_hand()
if hand.num_cards(defuse_card) > 0:
# Put card on top with 75% chance
if random_number.random() < 0.75:
return 0
else:
# Put in random place
pos = random_number.randint(0,nc)
return pos
else:
if random_number.random() < 0.5:
# Put card on bottom
return nc
else:
# Put it in a random place
pos = random_number.randint(0,nc)
return pos
play_game(MediumPlayer(), ScaredPlayer())
players = [SimplePlayer, ScaredPlayer, MediumPlayer]
M = np.zeros((len(players),len(players)))
count = 10000
for i in range(len(players)):
player0 = players[i]
for j in range(len(players)):
player1 = players[j]
if i!=j:
for k in range(count):
winner = play_game(player0(), player1(), print_details=False)
if winner == 0:
M[i,j] += 1
else:
M[j,i] += 1
for i in range(len(players)):
name0 = players[i]().name()
for j in range(i+1, len(players)):
name1 = players[j]().name()
print("{} won {}% versus {} who won {}%." \
.format(name0, M[i,j]*50/count, name1, M[j,i]*50/count))
for i in range(len(players)):
name0 = players[i]().name()
print("In total, {} won {}% of the time." \
.format(name0, np.sum(M[i])*100/(2*count*(len(players)-1))))
play_game(MediumPlayer(), HumanPlayer())