Christmas Coding Challenge – Uno
During this Christmas break I decided to have a go at implementing the card game Uno in Python. It’s a fun and simple game for all ages, and the rules are easy to pick up and follow – but there’s quite a challenge in implementing the rules in a program! I spent a few hours over a few evenings working on it and now have a completed version, and wanted to share some of the interesting bits of the code.
Uno is played with a bespoke card deck (not a normal deck of playing cards). Uno cards have a colour and a card type (a number or a symbol). There are four regular colour cards, and there are special cards which are black.
Each player is dealt a hand of 7 cards, and they take turns to play a card, trying to become the first player to get rid of all their cards. A player can only play a card if it has the same colour or card type as the last played card. If they cannot play a regular card but have a special black card, this can be played instead. Some cards have consequences, such as the next player having to pick up extra cards, players skipping turns or reversing order of play.
I used a test-driven development (TDD) approach to further my progress in building up the game from simple isolated card logic all the way to simulating gameplay in a multi-player game. I used pytest for its ability to verify exceptions were raised, but otherwise simply used Python’s built-in assert statement.
An Uno Card
I started by writing some tests to check errors were thrown if you tried to create an invalid card, then proceeded to creating valid cards. The card was implemented as an UnoCard class, whose objects stored a colour and a card type.
As well as validation on init, I added a method to determine whether a given card was playable on another card. The card playing rules are quite straightforward: you can place a card o
I started building up an UnoGame class, where a deck of UnoCard instances was created. The deck needed to contain a complete set of Uno cards, which I determined from this image on Wikipedia:
So it turns out there are digit cards 0-9 (and then 1-9 repeated) for each of the four colours, and two of each special card in each colour. There are also four of each of the two types of black cards.
I started by creating lists defining the colours and card types, and initially created the deck by combining these lists in a number of for-loops. However, I later refactored this to use product, repeat and chain from itertools in the standard library, which made it much neater:
color_cards = product(COLORS, COLOR_CARD_TYPES) black_cards = product(repeat('black', 4), BLACK_CARD_TYPES) all_cards = chain(color_cards, black_cards) deck = [UnoCard(color, card_type) for color, card_type in all_cards]
The UnoGame’s deck was then simply a list containing UnoCard objects.
In order for there to be a game, there needed to be players. I made UnoGame initialise with a given number of players. This meant a new UnoPlayer class. Each player would be dealt a hand of 7 cards from the shuffled deck. But first, I wrote tests and made sure that an UnoPlayer object could be created with 7 cards, and couldn’t be created without them.
In order for gameplay to take place, I needed to work out how I would designate a player as being the currently active player (whose turn it is). My first thought was itertools.cycle – a handy tool for infinitely iterating over an iterable object like a list, and just starting back at the beginning of the list once all items have been exhausted. However, Uno has a reverse card, meaning the order of play can be reversed at any moment.
I thought it through, and decided to design a new ReversibleCycle class to implement something like a cycle which could be reversed, as specified by the Uno rules. It took me quite a while to figure this out exactly, and get all my tests passing, but I was quite happy with the implementation I ended up with. A tricky part was the edge-case of what happens if the first card drawn (not played by a player – just the starting card) is a reverse card. According to the rules, play will start with the player to the right of what would have been the first player (i.e. the last player). Otherwise, in normal circumstances, the first player will play first, naturally.
I implemented the class as an iterator, so you could loop over it or manually
next() it. Example usage of my ReversibleCycle class:
>>> rc = ReversibleCycle(range(3)) >>> next(rc) 0 >>> next(rc) 1 >>> rc.reverse() >>> next(rc) 0 >>> next(rc) 2
I wrote tests of the ReversibleCycle in an isolated fashion. It works with any iterable, and I simply created an attribute within my UnoGame class which referred to an instance of ReversibleCycle, where the iterable was a list of UnoPlayer objects. I used a property to make it easy to look up which was the current player at any given moment.
Back to testing the gameplay, now I had a working model for cycling through players, with the ability to reverse order of play once a reverse card was played, I tested that players could play cards only when it was their turn, and that they could only play valid cards. I then tested that players playing cards would cause the correct consequences (e.g. the next player picks up 2, the new current player is the right one if a skip/reverse/etc. card is played, and so on).
I began testing gameplay by allowing the deck not to be shuffled, so that I had a predictable, testable order of cards. Looking back, I should have used a random seed instead (thanks for the tip, Dave). I took this game all the way to a player being designated the winner and the game ending.
I then proceeded to write some code, similar to my test code for the gameplay, which would automate play for all players, and end at some point declaring one of the players the winner. There was nothing clever about the way the players played – no strategy – I just determined whether they had any playable cards, and made them play the first playable one in their hand.
The last thing I did was to take the automated play code and embed it inside an AIUnoGame class, allowing for a single player to be controlled by the user using text input on request. The playable player would be shown their hand each round, and asked which card they would like to play. All actions of other players would be displayed (e.g. “Player 2 plays Red 5”, “Player 4 picks up 2”), and the winner would be declared as before. Again, nothing smart in the AI, but that’s something I may add later.
I had wondered when I started whether it would be feasible for me to create this as a GUI – a real visual playable game. I obviously wanted to start from a text-based interface, and get the logic of the game down before I worried about graphics, but again, it’s something I would like to look at next. I wonder if PyGame Zero or guizero would be suitable. Watch this space!
You can find my code on GitHub.