Game Simulation

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.

In [1]:
# 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

A card game

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.

A class for cards

Here is a class representing a card in the game.

In [2]:
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.

In [3]:
# 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.")
In [4]:
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()))
bomb: The player who draws this card loses unless they have a defuse card.

defuse: Having this card in your possession prevents a Bomb card from exploding.

skip: Playing this card allows you to immediately end your turn without drawing a card.

bottom: Playing will result in you immediately drawing a card from the bottom of the deck.

shuffle: Playing this card results in the draw pile being shuffled.

see: Playing this card will return a tuple consisting of the top three cards in the deck.

nothing: This card does nothing. You can't even play it.

A class for player hands

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.

In [5]:
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.

In [6]:
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))
h is a hand holding a defuse, and a skip.
h holds 2 cards.
h holds 1 defuse cards.
h2 is a hand holding a defuse, and a skip.
After removing a defuse card, h2 is a hand holding a skip. h is holding a defuse, and a skip.

Game Data

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 hand
  • gd.draw_pile_size(): the number of cards in the draw pile
  • gd.other_players_hand_count(): the number of cards in the other player's hand
  • gd.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.

In [7]:
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.

In [8]:
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)
GAME DATA: 10 cards remain in the draw pile.
           Your hand holds a defuse, and a shuffle.
           The other player holds 3 cards.
           The top card in the discard pile is see and the only other card is nothing.

Now imagining that the player plays a shuffle card, we'd update the data.

In [9]:
h.remove_card(shuffle_card)
discard_pile.append(shuffle_card)
gd._update(h, discard_pile)
print(gd)
GAME DATA: 10 cards remain in the draw pile.
           Your hand holds a defuse.
           The other player holds 3 cards.
           The discard pile has 3 cards.
           The top three cards are shuffle, see and nothing.

Abstract class for a player

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:

  • 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.

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.

In [10]:
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:

  • On a players turn, he/she just draws a card.
  • When the player defuses a bomb, he/she places it in a random place in the deck.
In [11]:
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:

  • When we defuse a bomb, we'll place the bomb on the top of the deck.
  • We'll keep track of if we are scared or not. We're scared if we just defused a bomb, or if the other player defused a bomb.
  • If we are not scared, we'll just take a card unless we have no defuse cards and a see card. If we have a see card, we'll use it and become scared if we see a bomb.
  • If we are scared, we'll do all we can to avoid taking the top card.

An implementation is below.

In [12]:
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

Function for playing a game:

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.

In [13]:
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

Playing Games:

Here is an example of a game played between two SimplePlayers.

In [14]:
player0 = SimplePlayer()
player1 = SimplePlayer()
play_game(player0, player1)
• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.
  ⁃ Simple1 picked up a bomb card!
  ⁃ Simple1 plays a defuse card.
  ⁃ Simple1 inserted the bomb card back in the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.
  ⁃ Simple0 picked up a bomb card!
  ⁃ Simple0 plays a defuse card.
  ⁃ Simple0 inserted the bomb card back in the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.
  ⁃ Simple0 picked up a bomb card!
  ⁃ Simple0 plays a defuse card.
  ⁃ Simple0 inserted the bomb card back in the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.

• It is now Simple1's turn.
  ⁃ Simple1 drew a card from the top of the draw pile.

• It is now Simple0's turn.
  ⁃ Simple0 drew a card from the top of the draw pile.
  ⁃ Simple0 picked up a bomb card!
  ⁃ Simple0 blew up!!!
  ⁃ Simple1 wins!!!
Out[14]:
1
In [15]:
play_game(SimplePlayer(), ScaredPlayer())
• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.
  ⁃ Simple picked up a bomb card!
  ⁃ Simple plays a defuse card.
  ⁃ Simple inserted the bomb card back in the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a bottom card.
  ⁃ Scared drew a card from the bottom of the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a bottom card.
  ⁃ Scared drew a card from the bottom of the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a bottom card.
  ⁃ Scared drew a card from the bottom of the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a skip card.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Simple's turn.
  ⁃ Simple drew a card from the top of the draw pile.
  ⁃ Simple picked up a bomb card!
  ⁃ Simple plays a defuse card.
  ⁃ Simple inserted the bomb card back in the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared blew up!!!
  ⁃ Simple wins!!!
Out[15]:
0

Play 10000 games between SimplePlayer and ScaredPlayer and see who does better.

In [16]:
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))
SimplePlayer won 20.989%, and ScaredPlayer won 79.011%.

Enabling a Human Player

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.

In [17]:
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.

In [18]:
play_game(ScaredPlayer(), HumanPlayer())
• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 16 cards remain in the draw pile.
           Your hand holds a defuse, a bottom, 2 shuffles, a see, and a nothing.
           The other player holds 7 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 14 cards remain in the draw pile.
           Your hand holds a defuse, a skip, a bottom, 2 shuffles, a see, and a nothing.
           The other player holds 8 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 12 cards remain in the draw pile.
           Your hand holds a defuse, a skip, a bottom, 2 shuffles, 2 sees, and a nothing.
           The other player holds 9 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 10 cards remain in the draw pile.
           Your hand holds a defuse, a skip, 2 bottoms, 2 shuffles, 2 sees, and a nothing.
           The other player holds 10 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 8 cards remain in the draw pile.
           Your hand holds a defuse, a skip, 2 bottoms, 2 shuffles, 3 sees, and a nothing.
           The other player holds 11 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Human's turn.
GAME DATA: 7 cards remain in the draw pile.
           Your hand holds a defuse, a skip, 2 bottoms, 2 shuffles, 4 sees, and a nothing.
           The other player holds 10 cards.
           The only card in the discard pile is defuse.
Last round your opponent played: defuse.
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).h
You play a shuffle card.
  ⁃ Human played a shuffle card.
  ⁃ The draw pile was shuffled.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a shuffle card.
  ⁃ The draw pile was shuffled.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 5 cards remain in the draw pile.
           Your hand holds a defuse, 2 skips, 2 bottoms, a shuffle, 4 sees, and a nothing.
           The other player holds 10 cards.
           The discard pile has 3 cards.
           The top three cards are shuffle, shuffle and defuse.
Last round your opponent played: shuffle.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.
  ⁃ Human picked up a bomb card!
  ⁃ Human plays a defuse card.
GAME DATA: 4 cards remain in the draw pile.
           Your hand holds 2 skips, 2 bottoms, a shuffle, 4 sees, and a nothing.
           The other player holds 10 cards.
           The discard pile has 4 cards.
           The top three cards are defuse, shuffle and shuffle.
Where do you want to place the bomb card? You may choose any integer in the interval [0, 4]. Zero corresponds to the top of the deck and 4 to the bottom.0
  ⁃ Human inserted the bomb card back in the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a skip card.

• It is now Human's turn.
GAME DATA: 5 cards remain in the draw pile.
           Your hand holds 2 skips, 2 bottoms, a shuffle, 4 sees, and a nothing.
           The other player holds 9 cards.
           The discard pile has 5 cards.
           The top three cards are skip, defuse and shuffle.
Last round your opponent played: skip.
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).b
You play a bottom card.
  ⁃ Human played a bottom card.
  ⁃ Human drew a card from the bottom of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a skip card.

• It is now Human's turn.
GAME DATA: 4 cards remain in the draw pile.
           Your hand holds 2 skips, a bottom, a shuffle, 5 sees, and a nothing.
           The other player holds 8 cards.
           The discard pile has 7 cards.
           The top three cards are skip, bottom and skip.
Last round your opponent played: skip.
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).b
You play a bottom card.
  ⁃ Human played a bottom card.
  ⁃ Human drew a card from the bottom of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a bottom card.
  ⁃ Scared drew a card from the bottom of the draw pile.

• It is now Human's turn.
GAME DATA: 2 cards remain in the draw pile.
           Your hand holds 2 skips, a bottom, a shuffle, 5 sees, and a nothing.
           The other player holds 8 cards.
           The discard pile has 9 cards.
           The top three cards are bottom, bottom and skip.
Last round your opponent played: bottom.
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).s
You play a see card.
  ⁃ Human played a see card.
The top card is a bomb card and the only other card is a shuffle card.
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).h
You play a shuffle card.
  ⁃ Human played a shuffle card.
  ⁃ The draw pile was shuffled.
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).s
You play a see card.
  ⁃ Human played a see card.
The top card is a bomb card and the only other card is a shuffle card.
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).k
You play a skip card.
  ⁃ Human played a skip card.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Human's turn.
GAME DATA: 2 cards remain in the draw pile.
           Your hand holds a skip, a bottom, 3 sees, and a nothing.
           The other player holds 7 cards.
           The discard pile has 14 cards.
           The top three cards are defuse, skip and see.
Last round your opponent played: defuse.
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).s
You play a see card.
  ⁃ Human played a see card.
The top card is a bomb card and the only other card is a shuffle card.
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).k
You play a skip card.
  ⁃ Human played a skip card.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Human's turn.
GAME DATA: 2 cards remain in the draw pile.
           Your hand holds a bottom, 2 sees, and a nothing.
           The other player holds 6 cards.
           The discard pile has 17 cards.
           The top three cards are defuse, skip and see.
Last round your opponent played: defuse.
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).b
You play a bottom card.
  ⁃ Human played a bottom card.
  ⁃ Human drew a card from the bottom of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared blew up!!!
  ⁃ Human wins!!!
Out[18]:
1

A slightly better player implementation

Here is another Player implementation that uses a slightly different strategy.

In [19]:
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
In [20]:
play_game(MediumPlayer(), ScaredPlayer())
• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Medium's turn.
  ⁃ Medium played a shuffle card.
  ⁃ The draw pile was shuffled.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a skip card.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a skip card.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a shuffle card.
  ⁃ The draw pile was shuffled.
  ⁃ Scared drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared drew a card from the top of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared plays a defuse card.
  ⁃ Scared inserted the bomb card back in the draw pile.

• It is now Medium's turn.
  ⁃ Medium played a shuffle card.
  ⁃ The draw pile was shuffled.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Scared's turn.
  ⁃ Scared played a bottom card.
  ⁃ Scared drew a card from the bottom of the draw pile.
  ⁃ Scared picked up a bomb card!
  ⁃ Scared blew up!!!
  ⁃ Medium wins!!!
Out[20]:
0
In [21]:
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))))
Simple won 21.65% versus Scared who won 78.35%.
Simple won 19.45% versus Medium who won 80.55%.
Scared won 34.07% versus Medium who won 65.93%.
In total, Simple won 20.55% of the time.
In total, Scared won 56.21% of the time.
In total, Medium won 73.24% of the time.
In [22]:
play_game(MediumPlayer(), HumanPlayer())
• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 16 cards remain in the draw pile.
           Your hand holds a defuse, a bottom, a see, and 3 nothings.
           The other player holds 7 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 14 cards remain in the draw pile.
           Your hand holds 2 defuses, a bottom, a see, and 3 nothings.
           The other player holds 8 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 12 cards remain in the draw pile.
           Your hand holds 2 defuses, a bottom, 2 sees, and 3 nothings.
           The other player holds 9 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 10 cards remain in the draw pile.
           Your hand holds 2 defuses, a bottom, 2 sees, and 4 nothings.
           The other player holds 10 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 8 cards remain in the draw pile.
           Your hand holds 2 defuses, a skip, a bottom, 2 sees, and 4 nothings.
           The other player holds 11 cards.
           The discard pile is empty.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.
  ⁃ Medium picked up a bomb card!
  ⁃ Medium plays a defuse card.
  ⁃ Medium inserted the bomb card back in the draw pile.

• It is now Human's turn.
GAME DATA: 7 cards remain in the draw pile.
           Your hand holds 2 defuses, a skip, a bottom, 3 sees, and 4 nothings.
           The other player holds 10 cards.
           The only card in the discard pile is defuse.
Last round your opponent played: defuse.
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).s
You play a see card.
  ⁃ Human played a see card.
The top card is a skip card, the second card is a shuffle card, and the third card is a see card.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.

• It is now Human's turn.
GAME DATA: 5 cards remain in the draw pile.
           Your hand holds 2 defuses, 2 skips, a bottom, 2 sees, and 4 nothings.
           The other player holds 11 cards.
           The top card in the discard pile is see and the only other card is defuse.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.
  ⁃ Medium picked up a bomb card!
  ⁃ Medium plays a defuse card.
  ⁃ Medium inserted the bomb card back in the draw pile.

• It is now Human's turn.
GAME DATA: 4 cards remain in the draw pile.
           Your hand holds 2 defuses, 2 skips, a bottom, 3 sees, and 4 nothings.
           The other player holds 10 cards.
           The discard pile has 3 cards.
           The top three cards are defuse, see and defuse.
Last round your opponent played: defuse.
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).s
You play a see card.
  ⁃ Human played a see card.
The top card is a bottom card, the second card is a bomb card, and the third card is a shuffle card.
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).
You draw a card.
  ⁃ Human drew a card from the top of the draw pile.

• It is now Medium's turn.
  ⁃ Medium played a see card.
  ⁃ Medium played a bottom card.
  ⁃ Medium drew a card from the bottom of the draw pile.

• It is now Human's turn.
GAME DATA: 2 cards remain in the draw pile.
           Your hand holds 2 defuses, 2 skips, 2 bottoms, 2 sees, and 4 nothings.
           The other player holds 9 cards.
           The discard pile has 6 cards.
           The top three cards are bottom, see and see.
Last round your opponent played: see, bottom.
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).b
You play a bottom card.
  ⁃ Human played a bottom card.
  ⁃ Human drew a card from the bottom of the draw pile.

• It is now Medium's turn.
  ⁃ Medium played a skip card.

• It is now Human's turn.
GAME DATA: one card remains in the draw pile.
           Your hand holds 2 defuses, 2 skips, a bottom, a shuffle, 2 sees, and 4 nothings.
           The other player holds 8 cards.
           The discard pile has 8 cards.
           The top three cards are skip, bottom and bottom.
Last round your opponent played: skip.
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).k
You play a skip card.
  ⁃ Human played a skip card.

• It is now Medium's turn.
  ⁃ Medium played a skip card.

• It is now Human's turn.
GAME DATA: one card remains in the draw pile.
           Your hand holds 2 defuses, a skip, a bottom, a shuffle, 2 sees, and 4 nothings.
           The other player holds 7 cards.
           The discard pile has 10 cards.
           The top three cards are skip, skip and skip.
Last round your opponent played: skip.
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).k
You play a skip card.
  ⁃ Human played a skip card.

• It is now Medium's turn.
  ⁃ Medium drew a card from the top of the draw pile.
  ⁃ Medium picked up a bomb card!
  ⁃ Medium blew up!!!
  ⁃ Human wins!!!
Out[22]:
1