Blog Image
TRICK-TAKING

Agram: Coding

When we started the CardStock project, we began with games that were short enough to quickly run simulations, yet allowed for elements of skillful play. Agram, a simple Nigerian trick-taking card game for 2 to 6 players, fit the bill perfectly. To get the blog started and show what we’ll be covering, here is an updated version of our analysis of Agram that appeared in Volume 2, Number 1, of Game & Puzzle Design.

First, we’ll cover how to code the rules in RECYCLE. Next post, we’ll examine some basic game statistics, followed by a post on advanced heuristics evaluated from AI game play, and finally, we’ll explore a few variants of Agram to see if we can make it better.

Rules

Agram uses a stripped French deck of only 35 cards, consisting of A3456789T in all suits, but removing the Ace of Spades. In Agram, players are dealt six cards from the deck, and play six tricks. To win a trick, players must follow the suit of the lead player with a higher card; there is no trump suit. The object of the game is to win the last trick.

RECYCLE Coding

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

First, the number of players are defined as 4, and the players are created. The teams are defined as individuals, indicating no alliances. The deck is instantiated to the invisible STOCK location which belongs to the game. Because the rules for Agram dictate that there is no Ace of Spades, two separate create deck calls were necessary.

(game
 (declare 4 'NUMP)
 (setup
  (create players 'NUMP)
  (create teams (0) (1) (2) (3))

  (create deck (game iloc STOCK) (deck (RANK (THREE, FOUR, FIVE, SIX, 
                                              SEVEN, EIGHT, NINE, TEN))
                                       (COLOR (RED (SUIT (HEARTS, DIAMONDS)))
                                              (BLACK (SUIT (SPADES, CLUBS))))))
  (create deck (game iloc STOCK) (deck (RANK (ACE))
                                       (COLOR (RED (SUIT (HEARTS, DIAMONDS)))
                                              (BLACK (SUIT (CLUBS)))))))

Next, the STOCK location, now containing all of the requisite cards, is shuffled, and 6 cards are dealt to each player into their invisible HAND location.

(do
 (
  (shuffle (game iloc STOCK))
  (all player 'P
       (repeat 6
               (move (top (game iloc STOCK))
                     (top ('P iloc HAND)))))))

Two nested stages are needed to play the game. The outer stage is over when all players have a HAND size of 0, so it will cycle for each trick in the game. The inner stage is over when all players have moved a card to their visible TRICK location, so it cycles through the players one at a time as they play a card to the trick. Because we need to know who owns the cards to determine who wins the trick, we don’t put the cards into one TRICK location belonging to the game. In physical card games, we can tell who played what card because of the orientation of the cards, as well as the player’s memory.

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

    (stage player
           (end
            (all player 'P
                 (> (size ('P vloc TRICK)) 0)))

Each player gets to choose a card to play for the trick, given that they follow the rules of play. There are three possible situations a player might be in when it is their turn.

  • First player, who can play anything
  • Following player, having a card of the same suit that has been led
  • Following player, unable to follow suit

We first handle the case in which at least the first card of the trick has been played and the current player is unable to follow suit. They are therefore allowed to play any card from their invisible HAND to their visible TRICK location.

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

Next, we handle the case in which at least the first card of the trick has been played and the player can (and therefore must) play a card which follows suit.

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

Finally, we give the first player the freedom to play any card, and subsequently remember that card to the LEAD location, ensuring that the following players will be forced into one of the two cases above.

 ((== (size (game mem LEAD)) 0)
  (any ((current player) iloc HAND) 'AC
       (do
           (
            (move 'AC
                  (top ((current player) vloc TRICK)))
            (remember (top ((current player) vloc TRICK))
                      (top (game mem LEAD))))))))))

With the trick playing complete, we return to the outer stage. The following do block creates a point map indicating how the cards are ranked. The LEAD suit is given higher precedence by adding 100 to each point value for those cards in that suit.

We then clear the LEAD memory location, set the next leader to be the player who holds the winning card, and moves the cards from the last trick into the visible DISCARD location.

In the event all of the players’ HAND locations are empty, indicating the game is over, the game awards 1 point to the player who won the most recent trick.

(do
    (
     ;; solidfy card precedence with LEAD suit
     (put points 'PRECEDENCE
          (
           ((SUIT (cardatt SUIT (top (game mem LEAD)))) 100)
           ((RANK (ACE)) 14)
           ((RANK (TEN)) 10)
           ((RANK (NINE)) 9)
           ((RANK (EIGHT)) 8)
           ((RANK (SEVEN)) 7)
           ((RANK (SIX)) 6)
           ((RANK (FIVE)) 5)
           ((RANK (FOUR)) 4)
           ((RANK (THREE)) 3)))

     ;; forget the LEAD card for next round
     (forget (top (game mem LEAD)))
     
     ;; determine who won the hand, set them first next time
     (cycle next (owner (max (union (all player 'P ('P vloc TRICK))) using 'PRECEDENCE)))

     ;; discard cards
     (all player 'P
          (move (top ('P vloc TRICK))
                (top (game vloc DISCARD))))

     ;; if that was the last round, give the winner a point
     ((all player 'P
           (== (size ('P iloc HAND)) 0))
      (inc ((next player) sto SCORE) 1)))))

Finally, the scoring code indicates that the winner of the game is the player with the highest value in their SCORE storage bin. From the prior rules we know that only one player will receive a point each game, making this a decidable and unique maximum value and owner.

(scoring max ((current player) sto SCORE)))

Go ahead and play a few rounds, and see what you think of how it works.

Up Next

In the next post, we’ll look at two main issues, does the player have choices, and is there opportunity for strategy? We’ll spin up some simulations with random and AI players, and see what happens!


comments powered by Disqus