Coding a Euchre Game, Part 7: Total Logic (Matt Gertz)

Coding a Euchre Game, Part 7: Total Logic (Matt Gertz)

  • Comments 1

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…!

Leave a Comment
  • Please add 2 and 2 and type the answer here:
  • Post