Beat Your Grandma at Cards - Chapter 4 - Game On
In this chapter, our game comes to life. We’ll cover our game architecture, and we’ll write up a simple randomized AI to play against.
The first thing we need is a representation of the game itself. We’ve discussed all the pieces already, but now we just need to put them all in one convenient place. We’ll call this thing a GameState:
You can see everything on this object is ‘final’. This means we can’t modify it, so every time something happens in the game, we have to create an entirely new GameState with the new information. It’s sort of like a snapshot of the game, and the game itself is represented by a series of GameStates with player actions in between.
Speaking of player actions, our GameState has methods for each action that players can take that I didn’t include in that code above. For example, when a player “orders up” the dealer card, we call this method:
You can see that this method creates an entirely new GameState, moving the game to the next phase (discard), and setting the bid winner and current player.
What about when players play cards?
This one is much more complicated, but the idea is the same: we create a new state with the new information. One important thing to call out in this method is that we need to be careful to never modify the data on our current state. So, for example, in order to remove the card being played from the current player’s hand, we first create a copy of the player, and then remove the card from the copy. Then, the next state uses that copied player.
There are a few more actions players can take, like discarding or calling trump, and they’re all represented on the GameState as functions. It’s a lot of code, mostly because we need to copy the entire GameState for every change, but the code tends to be simple. I won’t list all of the functions in this blog post, but check them out here.
Let’s have a brief interlude to discuss game architecture. We have a useful representation of the game itself, and we have a bag of functions that handle all the possible actions players can take, but we need some coordinator that maintains the state, shows the correct stuff on the screen, asks players to take their turns, and applies updates to the state when players are finished. It’ll look something like this:
Okay, so we just need to write that purple guy. Let’s start with just the first phase (the first bidding round), and move on from there. You’ll notice a few new concepts and classes in this code that we haven’t talked about, but don’t worry, we’ll break it all down.
The first big new thing here is the BidFirstRoundWidget. This is the widget responsible for showing the UI for the phase. Luckily, it’s incredibly simple because our EuchreTableWidget we designed in the last chapter does most of the heavy lifting:
This will render a screen that looks like this:
Notice that our controller passes “_onFirstRoundBidResult” into this widget? When the player clicks one of the two buttons, it will call that method with the bid result. Let’s implement the method now. The logic here is pretty simple. If the player picks it up, we move to the dealer discard phase, if the player passes, we just move to the next player, and if the bidding round is over, we go to the next bidding round:
You might be thinking at this point “alright, cool, but where does the AI fit into all this?” That’s a super pertinent question, and we’ll answer it now.
It’s probably clear that the Controller will be responsible for managing the AI. For example, if it’s the bidding phase, and it’s the AIs turn to bid, the Controller is the one to say “hey, AI player, let me know if you want to bid or not.” Then, when the AI responds, the controller will invoke “_onFirstRoundBidResult”. So let’s put a EuchreAI on our controller. We’ll start out with a fairly crummy AI named “DerpAI”:
Our AI needs to handle the primary actions that a Euchre player can take: bidding, discarding, and playing. Let’s create an interface:
So when does the controller ask the AI to do things? Well, one reasonable place is whenever the GameState changes. When the State changes, that means it’s a new person’s turn. We check if that person is an AI, and if it is, we ask her what she wants to do. Let’s add that function to our GameController:
And call the function in _onFirstRoundBidResult after our state changes:
And finally, we need an actual AI implementation! As noted, we’ll start with a “DerpAI”. This poor guy isn’t the best decision maker:
Alright, we’ve got our core components in place, and we’ve implemented one phase of the game in its entirety. In the next short chapter we’ll cover the remaining phases, then we’ll move on to a perfect information AI.