Coding a Euchre Game, Part 7: Total Logic (Matt Gertz)
Coding a Euchre Game, Part 7: Total Logic
Since I’ve been concentrating on specific VB functionality, you may have noticed that the one topic I haven’t really drilled into yet is game logic, and yet it’s central to what a game is all about. Games have certainly gotten more sophisticated over the years, and yet that sophistication is largely a result of graphical and audio advances. The actual logic of games itself hasn’t changed nearly as much – I still have to exercise pretty much the same control over my NWN2 party as I did way back in Pools of Darkness, for example, lest I get blown away by friendly fire.
For card games, the logic is well-defined and ages-old… sort of. Card games can have a lot of local variations in the rules, and Euchre is no exception. I grew up playing a particular set of rules in Michigan (24 cards, plus “Stick the Dealer” if all agreed), but my wife’s family in West Virginia plays with three less cards (getting rid of all 9’s except the nine of hearts) and always advances the deal if trump isn’t called in two bidding rounds. Obviously, if I wanted my game to be successful for both families, I was going to have to accommodate all of these options. Further research into Euchre showed me that there are all sorts of variations of Euchre, not only in the U.S. but around the world. I settled for the two choices I was familiar with (24 vs. 21 cards, “Stick the Dealer” vs. new hand), and added a couple that sounded interesting to me (“SuperEuchre,” which simply changes the number of points awarded if the defending team takes all of the trumps, and “Quiet Dealer,” which forces a dealer’s partner to play alone if he/she chooses the trump suit) – the latter options would be easy to implement by minor modifications to the logic.
For Euchre, there are two interesting areas of logic: bidding and playing, both as concern the AI players. (I could leverage that logic to “tutor” the human player as well, and maybe I’ll do so in a future version.) As you might guess, in either case the AI needs to understand the value of a given hand or card – I’ll refer to this as the “score,” though it has nothing to do with the actual scoring players receive when winning. The ordering of the cards for scoring is quite straightforward, but the relative distances between them can vary. Let me explain…
In Euchre, the ordering of cards (from most powerful to least powerful) goes as follows: the Jack of Trumps (Right Bower), the Jack of the other same-color suit (Left Bower), the Ace of Trumps, the King of Trumps, the Queen of Trumps, the Ten of Trumps, and (if it exists) the Nine of Trumps. These are followed by the normal ordering (A, K, Q, J, 10, 9) of any other non-trump suit. So, I could start out with an enum defining this ordering as follows:
Public Enum Values
NineNoTrump = 1
TenNoTrump = 2
JackNoTrump = 3
QueenNoTrump = 4
KingNoTrump = 5
AceNoTrump = 6
NineTrump = 7
TenTrump = 8
QueenTrump = 9
KingTrump = 10
AceTrump = 11
LeftBower = 12
RightBower = 13
End Enum
And that would seem to be all right. However, let’s consider the case where Bob is holding three non-trump aces, the queen trump, and the 10 trump(A A A QT 10T) and Alice is holding both bowers and the nine of trumps, in addition to a couple of non-trump nines (RT LT 9T 9 9). Bob "hand value" is 35, and Alice's is 34. Bob would seem to have a better hand, right? Wrong! The goal for the person who declares trump is to win three of the five “tricks” (hands). Both of Bob’s trumps would be neutralized by Alice’s superior trump cards, and Alice would still have one trump left to neutralize any of the aces in Bob’s hand. The only missing trumps (AT and KT), even if not owned by Alice’s partner, would also be covered by Alice’s bowers, making it highly certain that Alice would win her needed tricks. (Alice, of course, doesn’t know Bob’s cards, and so might still be reluctant to make a bid in case he’s got all of the rest of the trump.) In fact, players of Euchre are lucky if they can take a trick without using a trump or a non-trump ace, since the person who declares trump is likely to be deficient in non-trump cards, so cards other than those should be significantly lower in score.
The AI players, of course, are forbidden from knowing what each others' cards are (i.e., no peeking!), so I couldn't very well implement logic that would involve figuring out if high trumps would neutralize specific cards, etc. So, after some thinking about the probabilities involved, I revised the enum be more reflective of those probabilities. This is where testing is important – the game has to “feel right” when played. The user shouldn’t be thinking “why in the world did the AI choose trump with that hand?” Based on probability, testplay, and a strong familiarity with the game, I ended up with the following enum:
Public Enum Values
NineNoTrump = 1
TenNoTrump = 2
JackNoTrump = 3
QueenNoTrump = 4
KingNoTrump = 5
AceNoTrump = 10
NineTrump = 12
TenTrump = 15
QueenTrump = 20
KingTrump = 25
AceTrump = 30
LeftBower = 31
RightBower = 35
NoValue = -1
End Enum
and in this case, Bob would have a score of 65 and Alice would have a score of 80, which is much more reflective of the reality of the situation.
Now, we’ll need code to actually leverage this. Each AI will need to figure out what their hand score would be for a given trump suit. First, we need to deal with the incongruous case of the Jack of the same-color suit (which I’ll refer to as the Bower suit, even though that’s not technically accurate) being treated as trump suit:
Public Shared Function GetBowerSuit(ByVal Trump As Suits) As Suits
Select Case Trump
Case Suits.Hearts
Return Suits.Diamonds
Case Suits.Diamonds
Return Suits.Hearts
Case Suits.Clubs
Return Suits.Spades
Case Suits.Spades
Return Suits.Clubs
End Select
End Function
and then get the value of a hand (per-card) based on the given potential trump suit:
Public Function GetValue(ByVal Trump As Suits) As Values
If Suit = Trump Then
Select Case Rank
Case Ranks.Nine
Return Values.NineTrump
Case Ranks.Ten
Return Values.TenTrump
Case Ranks.Queen
Return Values.QueenTrump
Case Ranks.King
Return Values.KingTrump
Case Ranks.Ace
Return Values.AceTrump
Case Ranks.Jack
Return Values.RightBower
End Select
ElseIf Suit = GetBowerSuit(Trump) AndAlso Rank = Ranks.Jack Then
Return Values.LeftBower
Else
Select Case Rank
Case Ranks.Nine
Return Values.NineNoTrump
Case Ranks.Ten
Return Values.TenNoTrump
Case Ranks.Jack
Return Values.JackNoTrump
Case Ranks.Queen
Return Values.QueenNoTrump
Case Ranks.King
Return Values.KingNoTrump
Case Ranks.Ace
Return Values.AceNoTrump
End Select
End If
End Function
Private Function HandValue(ByVal TrumpSuit As EuchreCard.Suits) _
As Integer
HandValue = 0
Dim i As Integer
For i = 0 To 4
HandValue = HandValue + _
Me.CardsHeldThisHand(i).GetValue(TrumpSuit)
Next
End Function
and then compare the resulting score against a certain value to see if a bid on that suit is worthwhile. The values are created using the "scores" of the cards from the above enum, and are intended to approximate a hand on which a typical good player would be likely to bid:
' On class EuchreCard:
Public Const Loner As Integer = 117 ' Hand contains four good trump &
' one good support card; don’t
' need partner’s help
Public Const Makeable As Integer = 85 ' Can certainly get three tricks if
' partner has one good card also
' On class EuchrePlayer:
Private Function Makeable() As Integer
Select Case Personality
Case Personalities.Crazy
Return EuchreCard.Makeable - 15
Case Personalities.Normal
Return EuchreCard.Makeable
Case Personalities.Conservative
Return EuchreCard.Makeable + 15
End Select
End Function
Private Function Loner() As Integer
Select Case Personality
Case Personalities.Crazy
Return EuchreCard.Loner - 15
Case Personalities.Normal
Return EuchreCard.Loner
Case Personalities.Conservative
Return EuchreCard.Loner + 15
End Select
End Function
Note that this is where personalities come in on the option dialog I presented in the previous post in this series. The “conservative” personality won’t ever try to choose trump unless they’ve got a killer hand, whereas the “crazy” personality will try to call trump even if they’ve only got two cards. (This is the only place where I currently use the personalities, though I suppose I could contrive a way to use it on the actual cardplay.)
So, that’s the bidding AI, which is pretty straightforward. (There are a couple of extra bits involved for special cases, like knowing that a certain card got turned over in the kitty, but by and large that’s pretty much it.) Gameplay, however, is slightly trickier. For example, if Bob leads the more powerful card in the game, Alice (his opponent) certain isn’t going to play the second most powerful card if she can help it, since it would be wasted on that hand – she’ll try to save it for another hand. However, if Phil (Bob’s partner) has the second most powerful card, he certainly will want to play it so that Bob doesn’t have to worry about it coming back to bite him later in the game. So, let’s dig into gameplay a bit.
First, some more terminology: the first person to play a card is the leader. The first leader in a hand is the person to the left of the dealer (with play proceeding clockwise); the subsequent leader is whoever won the last trick. The team which called trump is the making team; the other team is the defending team. The making team needs to win three tricks in a hand to score points; if they fail, then they are set (or euchred), and the defending team scores points instead. The logic proceeds as follows:
(1) The leader plays his/her highest card (including trump) if on the making team, or the highest non-trump card if a defender. (Defenders don’t lead trump because the making team almost certainly has a higher trump).
(2) The second player will play the highest card (of the same suit) that beats the leader’s card. Why the highest? Well, if he/she plays the lowest card that beats the leaders card, the leader’s partner has a higher probability of beating his/her card. (As my wife’s great-grandma used to say, “Don’t send out a boy to do a man’s job.") If the second player doesn’t have a card of the same suit, then he/she will throw the lowest trump that will beat the leader’s card; if that’s not possible, then he/she just throws the lowest card available, reserving better cards for later tricks. If the player has a cards of the led suit, but the highest one can’t win, then he/she plays the lowest card of that suit, again reserving better cards for later tricks.
(3) The third player, who is the leader’s partner, has slightly more complicated logic. If the leader’s card got beaten by player number two, then player 3’s logic is just like player 2. However, if the leader is winning, there’s no point in player 3 wasting a high card, and so the player 3 plays the lowest card of that suit (or the lowest card overall, if none of the led suit are available).
(4) Player 4’s logic is very similar to that of player 3 – if player 2 (his/her partner) is already winning the trick, then play the lowest card that is allowed (led suit if possible, arbitrary lowest card otherwise); if not, then try to play a card that wins the trick.
This logic could be tightened up – if there’s a tie for lowest card, for example (i.e. two non-trump 9’s), then pick the suit that you have least of (to increase the chances of being able to play trump later), or if your partner leads a high trump, play a high trump so that your partner need not worry about the missing trump, etc., but I’ve found that the above rules are sufficient to make it seem “real” to me. Here’s what it looks like in code:
Private Function AutoPlayACard(ByVal Table As EuchreTable) As Integer
Dim index As Integer = -1
If Seat = Table.LeaderThisTrick Then
index = AutoLeadACard(Table)
ElseIf Seat = NextPlayer(Table.LeaderThisTrick) Then
index = AutoPlayDefendCard(Table)
ElseIf Seat = NextPlayer(NextPlayer(Table.LeaderThisTrick)) Then
index = AutoPlaySupportCard(Table)
Else
index = AutoPlayLastDefendCard(Table)
End If
Return index
End Function
Private Function AutoLeadACard(ByVal Table As EuchreTable) As Integer
Dim index As Integer = -1
If Seat = Table.PickedTrumpThisHand OrElse OppositeSeat() = _
Table.PickedTrumpThisHand Then
' Start off strong, and lead your highest value card:
index = HighestCard(Table)
Else
' Lead a high card which isn't trump
index = HighestCardNotTrump(Table)
If index = -1 Then
index = HighestCard(Table)
End If
End If
Return index
End Function
Private Function AutoPlayDefendCard(ByVal Table As EuchreTable) _
As Integer
Dim CurrentHighestValue As EuchreCard.Values = _
Table.PlayedCards( _
Table.LeaderThisTrick).GetValue(Table.TrumpSuit)
Dim index As Integer = HighestCardLedSuit(Table)
If index = -1 Then
' Don't have that suit -- try to trump it
index = LowestCardTrump(Table)
If index = -1 Then
' Don't have trump -- throw junk
index = LowestCard(Table)
End If
ElseIf Me.CardsHeldThisHand(index).GetValue(Table.TrumpSuit) _
< CurrentHighestValue Then
' Can't beat it -- throw lowest possible
index = LowestCardLedSuit(Table)
End If
Return index
End Function
Private Function AutoPlaySupportCard(ByVal Table As EuchreTable) _
As Integer
Dim CurrentLeaderValue As EuchreCard.Values = Table.PlayedCards( _
Table.LeaderThisTrick).GetValue(Table.TrumpSuit)
Dim CurrentDefenderValue As EuchreCard.Values = _
EuchreCard.Values.NoValue
If Not Table.Players( _
NextPlayer(Table.LeaderThisTrick)).SittingOutThisHand Then
CurrentDefenderValue = Table.PlayedCards(NextPlayer( _
Table.LeaderThisTrick)).GetCurrentValue( _
Table.TrumpSuit, Table.SuitLedThisRound)
End If
Dim Winning As Boolean = (CurrentDefenderValue <= CurrentLeaderValue)
Dim index As Integer = -1
If Not Winning Then
index = HighestCardLedSuit(Table)
If index = -1 Then
' Don't have that suit -- try to trump it
index = LowestCardTrumpThatTakes(Table, CurrentDefenderValue)
If index = -1 Then
' Don't have trump -- throw junk
index = LowestCard(Table)
End If
ElseIf Me.CardsHeldThisHand(index).GetValue(Table.TrumpSuit)_
< CurrentDefenderValue Then
' Can't beat it -- throw lowest possible
index = LowestCardLedSuit(Table)
End If
Else
' Don't overplay my partner
index = LowestCardLedSuit(Table)
If index = -1 Then
' Don't have that suit, just throw junk
index = LowestCard(Table)
End If
End If
Return index
End Function
Private Function AutoPlayLastDefendCard(ByVal Table As EuchreTable) _
As Integer
Dim CurrentLeaderValue As EuchreCard.Values = _
Table.PlayedCards( _
Table.LeaderThisTrick).GetValue(Table.TrumpSuit)
Dim CurrentDefenderValue As EuchreCard.Values = _
EuchreCard.Values.NoValue
If Not Table.Players(NextPlayer( _
Table.LeaderThisTrick)).SittingOutThisHand Then
CurrentDefenderValue = Table.PlayedCards(NextPlayer( _
Table.LeaderThisTrick)).GetCurrentValue( _
Table.TrumpSuit, Table.SuitLedThisRound)
End If
Dim CurrentSupporterValue As EuchreCard.Values = _
EuchreCard.Values.NoValue
If Not Table.Players(NextPlayer(NextPlayer( _
Table.LeaderThisTrick))).SittingOutThisHand Then
CurrentSupporterValue = _
Table.PlayedCards(NextPlayer(NextPlayer( _
Table.LeaderThisTrick))).GetCurrentValue( _
Table.TrumpSuit, Table.SuitLedThisRound)
End If
Dim Winning As Boolean = (CurrentDefenderValue>CurrentLeaderValue) _
AndAlso (CurrentDefenderValue > CurrentSupporterValue)
Dim index As Integer = -1
If Not Winning Then
Dim ValueToBeat As EuchreCard.Values = CurrentLeaderValue
If CurrentSupporterValue > CurrentLeaderValue Then
ValueToBeat = CurrentSupporterValue
End If
index = LowestCardThatTakesLedSuit(Table, ValueToBeat)
If index = -1 Then
index = LowestCardLedSuit(Table)
If index = -1 Then
index = LowestCardTrumpThatTakes(Table, ValueToBeat)
If index = -1 Then
index = LowestCard(Table)
End If
End If
End If
Else
' Don't overplay, you've already won
index = LowestCardLedSuit(Table)
If index = -1 Then
' Throw junk -- you've already won.
index = LowestCard(Table)
End If
End If
Return index
End Function
HighestCard(), LowestCard(), etc. do the obvious thing – they use the scoring enum from above to determine the relative strength of each remaining card in the player’s hand.
Note the reference to SittingOutThisHand. This state occurs when the person who selects trump decides (during the bid) that he or she does not need the partner’s help, and so the partner does not play. In such a case, AutoPlayACard does not get called for the player who is sitting out, and the logic for the players 3 and 4 needs to take that into account by not trying to look for a played card from that player, hence the usage above. (Since the leader always plays a card, by definition, and since no one plays a card before the leader, neither the leader nor player 2 -- if the latter even gets called -- will need to do a check for that state.)
The remaining code on the callstack (which I won’t list out here in detail; it's much simpler than the foregoing code and will be attached in the final post) simply wraps this all up:
- PlayTrick() calls AutoPlayACard() (or PlayACard(), for the human player) for each player and keeps track of who won the trick.
- PlayHand() determines who the initial leader is and then calls PlayTrick() five times, and then determines the scores from the tricks and updates them.
- PlayGame() runs the bidding rounds and then calls PlayHand() repeatedly until one team has 10 or more points. (Using the typical rules, a making team gets one point for making three tricks, two points for all five, and four points if the bidder went alone, and the defending team gets two points if they blocked the making team from winning at least three tricks.)
- StartItUp() gets the player options, initializes the deck of cards, and then calls PlayGame().
- NewGameInvoked() (discussed in a previous post) initializes the table and then calls StartItUp().
And that’s pretty much all there is to the logic!
Next time, I’ll be covering settings, rich edit controls, and help functionality, and then I’ll wrap up this series with a post on deploying the application. Until next time…!