Blog Image
TRICK-TAKING

Hearts: Coding

Our fifth game for analysis is a classic trick-taking game, Hearts! Hearts, and its family of trick-taking games, focus on avoiding tricks rather than collecting them, making them a nice counterpoint to other classics such as Whist and Spades. Hearts as played today was slowly evolved and mutated from earlier games, such as Black Maria, Knaves, Polignac, and SlobberHannes. Each of them still has the central goal of avoiding points, but they either alter the point values, remove card play restrictions, or find additional things to avoid. We’ll talk about these variants in later posts, but first, let’s see how you play Hearts!

Rules

Play proceeds in rounds, with each round consisting of thirteen tricks. Each round, shuffle a standard deck of cards. Each player receives thirteen cards. For each trick, players play one card to a central trick on the board. The first player will set the lead suit for the trick, which subsequent players must follow suit if they can, otherwise they may play any card from their hand (called being short of a suit). Also, the first player is restricted to not play a card from the Hearts suit unless one has already been played through short-suiting in a previous trick. Once all cards have been played, the player who played the highest card that matches the suit of the led card will win all the cards in the trick and become the first player for the next trick. Once all tricks have been played, players earn one point for each Heart collected in tricks, plus 13 points if they collected the Queen of Spades. If a player happens to collect all the Hearts and the Queen of Spades, then they will “Shoot the Moon” and instead subtract 26 points from their score. The game ends when one player earns a total of 100 points; at that point the player with the lowest point value wins the game.

RECYCLE Coding

To illustrate how we encode these rules computationally, we will walk through in detail the RECYCLE code for Hearts.

First, we create the players and the (increasingly irrelevant) teams, followed by the standard French deck of 52 cards. Initally these cards are added to the DISCARD location, to facilitate multiple rounds.

(game
 (setup  
  (create players 4)
  (create teams (0) (1) (2) (3))
  (create deck (game vloc DISCARD) (deck (RANK (A, TWO, THREE, FOUR, FIVE, SIX, 
                                                SEVEN, EIGHT, NINE, TEN, J, Q, K))
                                         (COLOR (RED (SUIT (HEARTS, DIAMONDS)))
                                                (BLACK (SUIT (CLUBS, SPADES)))))))       

The first stage of the game captures the large game loop, where players repeatedly play their hands until one player reaches a certain score, usually 100 points.

However, as coded here, the game will only play one round, saying that it is over when at least one player does not have a score of 0. This one round will help with our analysis, and also increase the speed of our simulations.

In the first iteration of coding this game for one round, we erroneously listed this as ending when any player has a score greater than 0. But this turned out to conflict with the “Shoot The Moon” scoring possibility, discussed later.

 (stage player
        (end 
         (any player 'P (!= ('P sto SCORE) 0)))

Each round, we need to move all the cards back to the STOCK from the DISCARD location.

We create a point map for scoring called 'SCORE, where each Heart is given a score of 1, and the Queen of Spades is given a score of 13.

The cards in the STOCK are shuffled, and each player is dealt 13 cards into their HAND.

Finally, we set an integer called BROKEN to track if any Hearts have been played. At the beginning of each round, this is set to 0.

(do 
    (
     (repeat all
             (move (top (game vloc DISCARD)) 
                   (top (game iloc STOCK))))
     (put points 'SCORE 
          (
           ((SUIT (HEARTS)) 1) 
           ((RANK (Q)) (SUIT (SPADES)) 13)))
     (shuffle (game iloc STOCK))
     (all player 'P
          (repeat 13
                  (move (top (game iloc STOCK))
                        (top ('P iloc HAND)))))
     (set (game sto BROKEN) 0)))
        

For the hand stage, we end when all the players have no cards left in their HAND location. Inside this stage, we have another stage, which cycles for the trick. It will end when each player has played one card to their TRICK location.

(stage player
       (end 
        (all player 'P 
             (== (size ('P iloc HAND)) 0)))
       
       (stage player
              (end 
               (all player 'P 
                    (== (size ('P vloc TRICK)) 1)))
                      

Recall in Agram, there were three options for a player in a trick taking game: play any card if the first player, follow suit if a following player, or play any card when unable to follow suit if a following player. In Hearts, the current player now has a choice between five distinct exclusive options. First, if they are the first player (determined by asking if there is a card in the memory location LEAD), and Hearts have not been broken (still has a value of 0), we try to make a filter that contains all cards from their HAND where the suit is not Hearts. When processed, these cards will get the temporary variable name 'C. The current player can play any one of these cards to their TRICK location.

After they play their card, they ask the game to remember it in the LEAD location, for everyone to reference as the trick progresses around the table.

  (choice  
   (         
    ((and (== (size (game mem LEAD)) 0)
          (== (game sto BROKEN) 0))
     (any (filter ((current player) iloc HAND) 'NH 
                  (!= (cardatt SUIT 'NH) HEARTS))
          'C     
          (do 
              (
               (move 'C  
                     (top ((current player) vloc TRICK)))               
               (remember (top ((current player) vloc TRICK)) 
                         (top (game mem LEAD)))))))
                        

Second, if they are the first player, and Hearts have not been broken, but they have no cards that are not Hearts, then they can play any card from their HAND to their TRICK location.

Again, after they play their card, they ask the game to remember it in the LEAD location, for everyone to reference as the trick progresses around the table.

((and (== (size (game mem LEAD)) 0)
      (== (game sto BROKEN) 0)
      (== (size (filter ((current player) iloc HAND) 'NH 
                        (!= (cardatt SUIT 'NH) HEARTS))) 0))

      (any ((current player) iloc HAND) 'C
         (do 
             (
                (move 'C 
                      (top ((current player) vloc TRICK)))
                  (remember (top ((current player) vloc TRICK)) 
                          (top (game mem LEAD)))))))
                        

Third, if they are the first player, but Hearts has been broken, then they can play any card from their HAND to their TRICK location.

Again, after they play their card, they ask the game to remember it in the LEAD location, for everyone to reference as the trick progresses around the table.

((and (== (size (game mem LEAD)) 0)
      (== (game sto BROKEN) 1))
      (any ((current player) iloc HAND) 'C
         (do 
             (
                (move 'C 
                      (top ((current player) vloc TRICK)))
                (remember (top ((current player) vloc TRICK)) 
                          (top (game mem LEAD)))))))

Fourth, if they are not the first player (determined by seeing that there is already a card in the LEAD memory location), and they cannot follow the suit of the card that was led, then they can play any card from their HAND to their TRICK location.

((and (== (size (game mem LEAD)) 1)
      (== (size (filter ((current player) iloc HAND) 'H 
                        (== (cardatt SUIT 'H) 
                            (cardatt SUIT (top (game mem LEAD)))))) 0))
 (any ((current player) iloc HAND) 'C
      (move 'C 
            (top ((current player) vloc TRICK)))))
                        

Finally, if they are not the first player, and they do have a card that can follow suit, then they can play one of these card with a matching suit to their TRICK location.

(any (filter ((current player) iloc HAND) 'H 
             (== (cardatt SUIT 'H)
                 (cardatt SUIT (top (game mem LEAD)))))
     'C
     ((== (size (game mem LEAD)) 1)
      (move 'C 
            (top ((current player) vloc TRICK))))))))
               

Once the inner trick stage ends, then the game determines the winner of the trick. We make another PointMap called 'PRECEDENCE to sort the cards from highest to lowest rank. In this map, we add in an extra 100 points for the suit that was led, so that this initial card and cards that follow suit will be ranked higher than other cards that did not follow suit.

With the map created, we no longer need to remember the LEAD card, so we forget it. Finally, we use the 'PRECEDENCE map to determine who won, by finding the owner of the card that gets the maximum value of all cards played to TRICK locations. This player is then set to be the next player in the cycle for this round stage, and will go first next trick.

   (do ( 
        (put points 'PRECEDENCE 
             (
              ((SUIT (cardatt SUIT (top (game mem LEAD)))) 100)
              ((RANK (A)) 14)
              ((RANK (K)) 13) 
              ((RANK (Q)) 12)
              ((RANK (J)) 11)
              ((RANK (TEN)) 10)
              ((RANK (NINE)) 9)
              ((RANK (EIGHT)) 8)
              ((RANK (SEVEN)) 7)
              ((RANK (SIX)) 6)
              ((RANK (FIVE)) 5)
              ((RANK (FOUR)) 4)
              ((RANK (THREE)) 3)
              ((RANK (TWO)) 2)))
        
        (forget (top (game mem LEAD)))
        (cycle next (owner (max (union (all player 'P 
                                            ('P vloc TRICK))) using 'PRECEDENCE)))
                    

If anyone played Hearts this trick, and Hearts has not been broken, then it is now broken by setting the BROKEN variable to 1.

Next, all players will move their TRICK card to the TRICKSWON location of the winning player for scoring.

((and (== (size (filter (union (all player 'P ('P vloc TRICK))) 
                 'PH (== (cardatt SUIT 'H) HEARTS))) 0)
      (== (game sto BROKEN) 0))
 (set (game sto BROKEN) 1))

(all player 'P
     (move (top ('P vloc TRICK)) 
           (top ((next player) vloc TRICKSWON)))))))
        

The 13 rounds are over, it is time to determine each player’s score. We make one more stage to cycle through the players, ending when they have each tabulated their own score.

If the current player has scored 26 points this turn, using the 'SCORE map from above, they have collected every Heart and the Queen of Spades, thus Shoot the Moon. In this case, their score will be decremented by 26 points. In all other cases of scoring less than 26, their points will be added to their SCORE. The last step of scoring is to move all their cards from TRICKSWON to the general DISCARD pile, to be ready for the next round.

(stage player
       (end 
        (all player 'P (== (size ('P vloc TRICKSWON)) 0)))
       (do 
           (                  
            ((== (sum ((current player) vloc TRICKSWON) using 'SCORE) 26)
             (dec ((current player) sto SCORE) 26))
            
            ((!= (sum ((current player) vloc TRICKSWON) using 'SCORE) 26)
             (inc ((current player) sto SCORE) (sum ((current player) vloc TRICKSWON) using 'SCORE)))
            
            (repeat all
                    (move (top ((current player) vloc TRICKSWON))
                          (top (game vloc DISCARD))))))))

Hearts, like Pairs, is a game where you win by having the least points.

 (scoring min ((current player) sto SCORE)))

Up Next

With the rules explained and coded, we can run simulations to gather basic statistics and evaluate the game play and feel with heuristics! Will this feel more like Agram as a trick-taking game, or Pairs as an avoid-getting-points game? More to come, thanks for reading!


comments powered by Disqus