Uno Lab
In this lab, we will consider a more complex game (with a more complex codebase), Uno.
The rules of Uno are pretty simple: Each player has a hand of cards. In the center of the table there is a face-down draw pile and a face-up play pile. On each player’s turn, she plays a card onto the play pile if possible, otherwise she draws a card from the draw pile. If the drawn card can be played, the player may choose to play it.
A card may be played if its color or rank matches the top card on the play pile. Some cards have special abilities which are activated when they are played:
- Skip (S) skips the next player’s turn.
- Reverse (R) reverses the order of play.
- Draw two (D) causes the next player to draw two cards.
- Wild (W) may always be played. When you play a wild, assign it a color of your choice.
- Wild draw four (X) is like wild, but additionally the next player draws four cards and loses their turn.
The first player to play all their cards wins.
Play the game
Start learning how this package works by trying it out. Enter a poetry shell and then run python play_uno.py
.
You will see an Uno game start in your Terminal. Play for a bit until you have the hang of it.
Poking around
You don’t need to understand all the code in this package to complete this lab’s coding challenges,
but you do need a sense of how the parts all fit together.
Let’s look at play_uno.py
:
|
|
The first thing to notice is that there are quite a few import statements.
This package follows a pattern familiar from tic-tac-toe: models, views, and players.
Run tree .
to see a listing of all the files in this project:
$ tree .
├── play_uno.py
├── poetry.lock
├── pyproject.toml
├── test_uno.py
└── uno
├── decks.py
├── model
│ ├── card.py
│ └── game.py
├── player
│ ├── computer.py
│ └── human.py
└── view
├── static
│ └── style.css
├── templates
│ ├── playable_card.html
│ ├── playable_wild_card.html
│ └── uno.html
├── terminal.py
└── web.py
💻
By default, Uno uses a TerminalUnoView
, but it also comes with
a WebUnoView
. Edit play_uno.py
to use the web view instead of the Terminal view.
What happens?
Implementing special cards
💻
Currently, play_uno.py
initializes a game using a basic deck which doesn’t contain
any special cards (line 13). The second argument to UnoGame
is the name of the deck to use.
Edit play_uno.py
to use the "standard"
deck instead of the "basic"
deck.
When you play again, you will see that you sometimes get special cards now. Wild cards work as expected
(you can always play them, and you can choose their color), but otherwise special cards don’t have the
special effects they are supposed to have. The reason for this is that the Card
model is incomplete.
💻
Fix uno/model/card.py
so the special cards work. Here is a brief description of
how the Card
class interacts with the UnoGame
class, which will help with this task.
- The
Card
class models a single card. When initializing aCard
, you must provide a two-character card code (e.g.R3
for the red three card, orYR
for the yellow reverse card.Card
provides useful methods such as determining the card’s color, its rank, and determining whether it can be played on a given top card. - On a player’s turn, after she has chosen an action,
UnoGame.play_action
is called to update the game’s state in response to the action. As part of this method, any time a card is played, the card’sactivate
method is called after the current player’s turn ends. Card.activate
receives a single argument, theUnoGame
, and then is free to modify the game as necessary. As an example, the special effect for a skip card is already implemented.def activate(self, game): "Activates the card's special ability by making a change to the game." if self.rank == "S": player = game.current_player() game.messages.append(f"{player.name}'s turn is skipped!") game.end_of_turn()
- In the code above, we look up the current player (this will be the player after the player who played the card, since their turn just ended), and end their turn. This has the effect of skipping that player.
- In addition to enacting the card’s special effects on the game, you need to add a string to the
game.messages
array explaining what happened. Each view usesgame.messages
to keep players updated on what’s happening. - You will need to use the methods and attributes of
UnoGame
to implement the rest of the special cards. You don’t need to understand all the code, but you will need to skimuno.model.game
to learn which methods are available and how to use them. - There is a special deck called
"special"
containing only special cards. This doesn’t make for a fun playing experience, but it’s very useful for debugging and testing your solution.
A smarter computer player
As with tic-tac-toe, the built-in computer player (uno.player.computer.RandomUnoPlayer
) just chooses
random moves. Can you do better? Unfortunately, the lookahead strategy we used in the tic-tac-toe lab
won’t work for a few different reasons:
- Uno has chance. When you draw a card you don’t know what you will get. In the lookahaed strategy, we considered the reward which would come from each action and chose the best one, but now we don’t know the result of an action.
- We already saw with Nim that looking ahead to the end of the game starts to get really complex really quickly. Uno is much more complex and a game potentially lasts for many turns. There is no way we could explore all the possible states in future games.
💻 Think about the strategy you use to play uno and implement it in a new computer player.
- Create a new Python file, something like
uno/player/smart.py
and paste in the contents ofuno/player/computer.py
. - Edit the
choose_action
method to use a different strategy for choosing and returning an action. This method receives two arguments:state
is a dictionary containing the following keys:- “hand”: a list of
Card
s in the player’s hand. - “top_card”: a
Card
which the top card on the play pile. - “opponent_hands”: A list of integers showing how many cards each opponent has.
- “clockwise”: A boolean showing whether play is currently clockwise.
- “drawn_card: When the player has just drawn a playable card, this is a
Card
. Otherwise, this isNone
.
- “hand”: a list of
actions
is a list of possible actions. An action is a dictionary containing the key “action”, whose values may be “play,““draw,” or “pass.” (“pass” is only a valid action when the player has just drawn a card which could be immediately played. If “action” is “play”, the dictionary will also have a “card” key. If the card is a wild card, then the dictionary will additinally have a “color” key. Examples include:{"action": "draw"}
{"action": "play", "card": "R4"}
{"action": "play", "card": "X4", "color": "BLUE"}
{"action": "pass"}
💻
Test your player. test_uno.py
contains a script which has your computer player
play 1000 games against three random-playing opponents and reports how many times it won. If you test
the random computer player, it’s no surprise that it wins about 25% of the time:
$ python test_uno.py uno.player.computer.RandomUnoPlayer
Result: uno.player.computer.RandomUnoPlayer won 259/1000 games.
See if you can write a strategy which wins at least 40% of games.