Tutorial 7: Naive War (card game)
Setup: Before You Start
Create a new empty project in Spyder, and call it
Tutorial_7
.
Now create a new program file, war.py
. All the functions
you write have to be added to this file.
Context and Objectives
We are going to implement the basic functionalities for a card game.
At the end of the mandatory part of the tutorial, you are going to be one step away from being able to play War, which is a card game which seems to be almost globally known. War is its most common name; you may know it as Battle. In this tutorial we will implement a naïve version of it.
We will play a game, where each player play simultaneously the first card of its deck, and the player with the highest card wins all of them and place at the end of their deck in a random order. To handle draws, we are going to consider the classical order of suits: Clubs ♣ < Diamonds ♦ < Hearts ♥ < Spades ♠. The game ends when a player has all the cards.
Skills we will practice today: writing classes, creating objects, interactions between different classes/objects.
Exercises
Exercise 1: A Class for Playing Cards
We will play with the usual French playing cards, where a deck consists of four suits of thirteen cards each.
Create a new class Card
in your
war.py file. Your class should begin as follows:
class Card:
"""French playing cards.
Class attributes:
suit_names -- the four suits Clubs, Diamonds, Hearts, Spades
rank_names -- the 13 ranks in each suit: Two--Ten, Jack, Queen, King, Ace
Data attributes:
suit, rank -- the Card's suit and rank, as indices into the lists above
"""
= ['Clubs', 'Diamonds', 'Hearts', 'Spades']
suit_names = ['Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight',
rank_names 'Nine', 'Ten', 'Jack', 'Queen', 'King', 'Ace']
Add an __init__()
method to the
Card
class. This method has three parameters:
self
(the current/active object itself), suit
,
and rank
. It initializes the data attributes of the object
with the same names. Your method should begin as follows:
def __init__(self, suit, rank):
suit
represents an index into thesuit_names
list (an integer between 0 and 3);rank
represents an index into therank_names
list (an integer between 0 and 12);
Once your __init__()
method is defined, you should be
able to run the following test:
In [1]: c = Card(2, 12)
In [2]: c.suit
Out[2]: 2
In [3]: c.rank
Out[3]: 12
In [4]: c.suit_names[c.suit]
Out[106]: 'Hearts'
Add a __str__()
method to the
Card
class, which uses the object’s suit
and
rank
attributes as indices into the corresponding lists,
i.e. suit_names
or rank_names
, in order to
return a readable card value. Your method should begin as follows:
def __str__(self):
Once this is done, you should be able to run the following simple test:
In [5]: c = Card(0, 0)
In [6]: str(c) # str(c) uses c.__str__(), if it exists
Out[6]: 'Two of Clubs'
In [7]: print(c) # equivalent to print(str(c))
Two of Clubs
In [8]: print(Card(3, 12))
Ace of Spades
Upload your file war.py
:
Interlude: Some Cards Are More Equal Than Others
To play the game, we will need to be able to check whether or not two card objects represent the same playing card.
Equality Testing with
__eq__
: The Problem
When Python compares the equality of two objects (of any class), say
obj1 == obj2
, then it will search for a method named
__eq__
in obj1
. The __eq__
method
should take two arguments, self
(the active object,
obj1
in this case) and other
(a name for the
other object being compared, obj2
in this case), and return
True
or False
, depending on whether the
objects should be considered equal or not.
If obj1
has no __eq__
method but
obj2
does, then Python will call obj2
’s
__eq__
method to compare equality (and then
self
will be a name for obj2
and
other
a name for obj1
).
If neither obj1
or obj2
has a
__eq__
method, then by default Python will simply
test whether obj1
and obj2
are names for
the same object in memory. This means that if obj1
and
obj2
were created separately, then
obj1 == obj2
will be false, even if the objects have
exactly the same attribute values. Often this is not the behaviour
that we want! For a more meaningful equality test, we must define an
__eq__
method.
For example, before adding an __eq__
method to your Card
class, you should see the following
behaviour:
In [9]: c1 = Card(0, 0)
In [10]: c2 = Card(0, 0)
In [11]: c1 == c2 # **before** __eq__ is defined in Card
Out[11]: False
This is because c1
and c2
are distinct
objects, at different addresses in memory:
In [12]: c1
Out[12]: <__main__.Card at 0x7fa5f46a7278>
In [13]: c2
Out[13]: <__main__.Card at 0x7fa5f46a7a90>
The Solution
We will now add a method __eq__()
to
the Card
class, declaring quite reasonably that two cards
are equal if (and only if) their ranks and suits are equal. To this end,
add the following method to your class, which provides a
deep equality check for cards:
def __eq__(self, other):
return self.rank == other.rank and self.suit == other.suit
We should then see
In [14]: c1 = Card(0, 0)
In [15]: c2 = Card(0, 0)
In [16]: c1 == c2 # **after** __eq__ is defined in Card
Out[16]: True
Upload your file war.py
:
Inequality Testing with
__gt__
:
To find the highest card, a natural approach is to try to find the
max()
. Python inner mechanisms, for that use the comparison
operator >
defined for the class. Like for equality
testing, you can override its behavior by once again introducing a new
method __gt__
(for greater than). That tests which of two
cards is the highest, by first comparing their rank, and in case of
equality by comparing their suits.
def __gt__(self, other):
# returns True if and only if this Card is higher than the other
We should then see
In [17]: c1 = Card(1, 0)
In [18]: c2 = Card(0, 0)
In [19]: c1 > c2
Out[19]: True
In [20]: c1 = Card(1, 0)
In [21]: c2 = Card(1, 1)
In [22]: c1 > c2
Out[22]: False
In [23]: c1 = Card(1, 0)
In [24]: c2 = Card(0, 2)
In [25]: c1 > c2
Out[25]: False
Upload your file war.py
:
Exercise 2: A Class for Decks of Cards
The next thing we need is a class to represent a full deck of cards.
Create a new class Deck
(still in
war.py
), which should begin as follows:
class Deck:
"""A deck of Cards.
Data attributes:
cards -- a list of all Cards in the Deck
"""
Add an __init__()
method to your
Deck
class. This should fill the object’s
cards
attribute with all the Cards
in the
Deck
, in the standard deck ordering with “aces high”: Two
of Clubs, Three of Clubs, …, King of Clubs, Ace of Clubs, Two of
Diamonds, …, Ace of Diamonds, Two of Hearts, …, Ace of Hearts, Two of
Spades, …, Ace of Spades.
For testing purposes, your __init__()
method should take
an extra integer parameter minrank
that gives the minimal
rank of the cards in the deck. Hence, your __init__()
method should begin as follows:
def __init__(self, minrank):
We will use minrank
to create decks of cards which do
not contain some of the smaller cards. Thus, Deck(n)
should
create a deck which does not contain the cards with a rank (strictly)
less than n
.
Hint: Using len(Card.rank_names) -1
is a good way to
access the number of ranks in the current card games.
Also add a __str__()
method to your
Deck
class. This should return the cards in a
Deck
object in the order they appear in its
cards
attribute, separated by the string ', '
.
Your method should begin as follows:
def __str__(self):
Hint: You might want to implement your method by
first creating a list l
containing the string
representations of all cards in the deck and then return
', '.join(l)
.
You can quickly test your __init__()
and __str__()
methods in Deck
using the
following example:
In [26]: d = Deck(10)
In [27]: print(d)
Queen of Clubs, King of Clubs, Ace of Clubs, Queen of Diamonds, King of Diamonds, Ace of Diamonds, Queen of Hearts, King of Hearts, Ace of Hearts, Queen of Spades, King of Spades, Ace of Spades
Upload your file war.py
:
Exercise 3: More about Decks
Add a method pop()
to your
Deck
class which removes and returns the last card
in the deck. You may assume that the deck is not empty before
pop()
is called. We will need pop()
later for
dealing cards. Your method should start as follows:
def pop(self):
"""Remove and return last card from deck."""
You can quickly test your pop()
method
using this example:
In [28]: d = Deck(10)
In [29]: print(d)
Queen of Clubs, King of Clubs, Ace of Clubs, Queen of Diamonds, King of Diamonds, Ace of Diamonds, Queen of Hearts, King of Hearts, Ace of Hearts, Queen of Spades, King of Spades, Ace of Spades
In [30]: c = d.pop()
In [31]: print(c)
Ace of Spades
In [32]: print(d)
Queen of Clubs, King of Clubs, Ace of Clubs, Queen of Diamonds, King of Diamonds, Ace of Diamonds, Queen of Hearts, King of Hearts, Ace of Hearts, Queen of Spades, King of Spades
Finally, we need to be able to shuffle a deck of cards.
Add a method shuffle
to your
Deck
class which shuffles a deck’s Cards
randomly. To this end, add import random
to the beginning of your war.py
file, and then use the
shuffle
method of the random
module
Your method should start with
def shuffle(self):
"""Shuffle the deck."""
Hint: If l
is a list, then
random.shuffle(l)
shuffles the list in place (that
is, it modifies l
and returns None
).
Now you should see something similar to the following (your output may differ due to randomization).
In [33]: d = Deck(10)
In [34]: print(d)
Queen of Clubs, King of Clubs, Ace of Clubs, Queen of Diamonds, King of Diamonds, Ace of Diamonds, Queen of Hearts, King of Hearts, Ace of Hearts, Queen of Spades, King of Spades, Ace of Spades
In [35]: d.shuffle()
In [36]: print(d)
Ace of Spades, King of Clubs, Queen of Spades, King of Diamonds, Queen of Clubs, Queen of Hearts, Queen of Diamonds, Ace of Hearts, King of Spades, King of Hearts, Ace of Clubs, Ace of Diamonds
Upload your file war.py
:
Exercise 4: A Class for Players
We need to represent players of the card game. To this end,
add a class Player
to your code. Your
class should start as follows:
class Player:
"""A player of the card game.
Data attributes:
name -- the name of the player
hand -- a Deck containing composed of the player's cards (their "hand")
"""
Add an __init__()
method to your class
which initializes the object’s name
to the given string and
the object’s hand
list to an empty Deck. Hence your
__init__()
method should begin as follows:
def __init__(self, name):
Add a __str__()
method to your class
which returns a string consisting of the string 'Player '
,
followed by the player’s name, and then either a string representing the
player’s cards (separated by ', '
) or the string
'has no cards'
if the player has no cards. Your
__str__()
method should begin as follows:
def __str__(self):
For example, in the console we should see
In [37]: p = Player('Adam')
In [38]: print(p)
Player Adam has no cards
In [39]: p.hand = Deck(12)
In [40]: print(p)
Player Adam has: Ace of Clubs, Ace of Diamonds, Ace of Hearts, Ace of Spades
It should be noted, that from now on, using
player.hand.cards
is a neat shortcut to access the list of
cards in the player player
hand.
Upload your file war.py
:
Exercise 5: Players Playing
We need the following additional methods for the Player
class:
- a method
add_card()
which adds a card, given as input, at the beginning of the player’s hand; - a method
num_cards()
which returns the number of cards in the player’s hand; - finally, a method
remove_card()
which returns the bottom (i.e., last) card from the player’s hand.
Your methods should begin as follows:
def add_card(self, card):
"""Add card to this player's hand."""
def num_cards(self):
"""Return the number of cards in this player's hand."""
def remove_card(self):
"""Remove the last card from this player's hand and return it."""
Test your Player
class. In the console
we expect the following:
In [41]: p = Player('Liza')
In [42]: p.num_cards()
Out[42]: 0
In [43]: p.add_card(Card(2, 4))
In [44]: p.num_cards()
Out[44]: 1
In [45]: print(p)
Player Liza has: Six of Hearts
In [46]: c = p.remove_card()
In [47]: print(c)
Six of Hearts
In [48]: print(p)
Player Liza has no cards
Upload your file war.py
:
Exercise 6: A Class for Card Games
The last class we need is one which represents a card game.
Create a new class CardGame
, which should
begin as follows:
class CardGame:
"""A class for playing card games.
Data attributes:
players -- a list of Player objects which participate in the game
deck -- a Deck of Cards used for playing
num_cards -- the number of Cards in the game
"""
Add __init__()
and
__str__()
methods to your CardGame
class.
The __init__
method should begin as follows:
def __init__(self, player_names, minrank):
__init__
takes as input a list of player names and an
integer minrank
like the Deck
class. The
method then initializes a list players
of Player objects.
Further, __init__()
creates a Deck object deck
using Deck(minrank)
, and assigns to num_cards
the number of cards in the deck, for later use.
The __str__()
method should begin as follows:
def __str__(self):
__str__
returns a string representing each player and
all of their cards, with each player (and their cards) separated by a
newline character ('\n'
), (implicitly) using the
Player
class’s __str__()
method.
Add a burn_cards()
method to your
CardGame
class which removes a list of cards
cards
(passed as a parameter) from the game’s deck.
Your methods should start as follows:
def burn_cards(self, cards):
"""Remove the cards 'cards' from this game's deck for those that exists,
and update the number of cards in the deck accordingly"""
Test your work. In the console, we expect the following behavior:
In [49]: g = CardGame(['Grace', 'Emmy', 'Sofia'], 11)
In [50]: print(g)
Player Grace has no cards
Player Emmy has no cards
Player Sofia has no cards
In [51]: g.num_cards
Out[51]: 8
In [52]: print(g.deck)
King of Clubs, Ace of Clubs, King of Diamonds, Ace of Diamonds, King of Hearts, Ace of Hearts, King of Spades, Ace of Spades
In [53]: g.burn_cards([Card(3,12)])
In [54]: g.num_cards
Out[54]: 7
In [55]: print(g.deck)
King of Clubs, Ace of Clubs, King of Diamonds, Ace of Diamonds, King of Hearts, Ace of Hearts, King of Spades
It should be noted, that as for players, the list of cards in a deck
deck
can be accessed via deck.cards
.
Upload your file war.py
:
Exercise 7: Dealing Cards
Add a shuffle_deck()
method to your
CardGame
class which shuffles the game’s deck, and a
deal_cards()
method which deals all cards in the deck to
the players. The cards are dealt in reverse order (that is, starting
from the end of the deck), in a round-robin fashion, starting with
players[0]
, until the deck is empty. Use the
Player
class’s add_card()
method and the
pop()
method in the Deck
class.
Your methods should start as follows:
def shuffle_deck(self):
"""Shuffle this game's deck."""
def deal_cards(self):
"""Deal all of the cards in the deck to the players, round-robin."""
Test your work (your output may differ due to randomization):
In [56]: g = CardGame(['Grace', 'Emmy', 'Sofia'], 10)
In [57]: g.shuffle_deck()
In [58]: g.deal_cards()
In [59]: print(g)
Player Grace has: King of Hearts, Queen of Spades, Ace of Diamonds, King of Clubs
Player Emmy has: Ace of Clubs, Queen of Diamonds, Queen of Hearts, Ace of Hearts
Player Sofia has: Ace of Spades, King of Diamonds, King of Spades, Queen of Clubs
In [60]: print(g.deck)
Upload your file war.py
:
Exercise 8: Playing “Find the highest”
We can now implement our simple card game.
Add a simple_turn()
method to your
CardGame
class which plays the last card of the deck of
each active player, and declares the owner of the highest card as the
winner.
More precisely, your function must return
the name of
the winner, and the list of played Cards in the same order. One might
assume that the list of active players is up to date, so each player has
at least one card in their respective deck.
Your method should start as follows:
def simple_turn(self):
"""Play a very simple game.
For each player, play the first card.
The winner is the player with the highest cards.
"""
Test your work. In the console we expect the following:
In [61]: g = CardGame(['Grace', 'Emmy', 'Sofia'], 9)
In [62]: g.deal_cards()
In [63]: (winner, trick) = g.simple_turn()
Player Grace: Jack of Clubs
Player Emmy: King of Spades
Player Sofia: Queen of Spades
Player Grace wins the round
In [64]: winner
Out[64]: 'Grace'
Play some more simple games. (Use the shuffle_deck()
method to get less predictable outcomes…)
Upload your file war.py
:
Exercise 9: Playing this simple Game
Now you can add a method play_simple()
to your CardGame
that plays a simple turn, add those cards
in a random order at the beginning of the winner’s deck, and
repeat until only one player has all the cards. You should be careful
and properly handle players with no cards.
Test your work. In the console we expect the following:
In [65]: g = CardGame(['Grace', 'Emmy', 'Sofia'], 10)
In [66]: g.burn_cards([Card(3,12)])
In [67]: g.deal_cards()
In [68]: g.play_simple()
...
Out[68]: 'Emmy'
Upload your file war.py
:
These exercises should not require you to alter the previous functions.
Exercise 10(Optional): Handle Wars
We can now implement the real game.
Not so Simple turn
Add a war_turn()
method to your
CardGame
class which takes a list of players and plays the
first card of their deck, and declares the owner of the highest card as
the winner, however in war we disregard suits, so there
could be more than one winner. So the function returns
a
list of winners, and the list of played cards.
Full on war
Add a war()
method to your
CardGame
class which now unrolls a full turn of war.
- Call
listw,pot=war_turn()
- If there is a sole winner, add the cards from
pot
totheir
deck - Otherwise
- Make each of the potential winner add their first card to the
pot
- Restart a new
war_turn
for them, and iterate until there is a sole winner
- Make each of the potential winner add their first card to the
Your method should start as follows:
def war(self,listplayer):
"""Plays a turn of war between active players.
In case of equality amongst the winning cards
Each potential winner, add the first card to the pot.
Play the second card, and compare them.
Iterate if need be.
The function return the winner, and the pot (list of card played)
"""
War
Finally, add a method play()
to the
CardGame
class which plays the War game.
Repeat the war_turn()
method until there is only one
active player. It is recommended to have a list of active players, and
update it every time a player loses (cannot play a card), to easily
detect a winning condition.