A Rubber Bridge Game with AI Players

Contract bridge is a hard game for AI. In chess or Go both players see the whole board. In bridge each player sees only 13 of the 52 cards. Partners must share what they hold through a small, formal bidding language, and they must work together against two opponents who do the same. In this chapter we build a complete rubber bridge program in Common Lisp. It has rule-based bidding, heuristic card play, a prototype Monte Carlo player, and full rubber bridge scoring. You play South in a text UI and the program plays the other three seats.

The example code is in the directory src/Bridge_game.

How to Play Bridge in One Page

Four players form two teams: North-South and East-West. The dealer deals the 52-card deck evenly, so each player holds 13 cards.

The auction. Starting with the dealer, players bid in turn, clockwise. A bid such as “2 Hearts” promises to win at least Code Test tricks, here 8, with hearts as the trump suit. Each bid must top the last one, either at a higher level or with a higher-ranking suit at the same level. Suits rank from low to high: clubs, diamonds, hearts, spades, and then no trump. A player may also pass, double an opponent’s bid, or redouble an opponent’s double. The auction ends after three passes in a row that follow at least one suit bid. The last bid becomes the contract. A double or redouble applies to the contract only if it comes after that bid. A later bid cancels a double of an earlier bid.

Declarer, dummy, and defenders. On the side that won the auction, the first player to bid the contract’s suit becomes the declarer. Declarer’s partner is the dummy. After the opening lead, dummy lays their hand face up and declarer plays both hands. The other team are the defenders.

Trick play. The player to declarer’s left makes the opening lead. Each player must follow the suit led if they can. If they can not, they may play any card, including a trump. The highest card of the suit led wins the trick, unless someone played a trump, in which case the highest trump wins. The winner of each trick leads to the next one. After all 13 tricks, we score the deal.

Hand evaluation. Players judge a hand with high card points (HCP): Ace 4, King 3, Queen 2, Jack 1. The deck holds 40 HCP. Shape adds value. Before partners find a trump fit, each card past four in a suit adds one length point. After they find a fit, short suits add shortness points instead: void 3, singleton 2, doubleton 1. A game contract usually needs 25 or 26 points between the two hands.

Rubber bridge scoring. A rubber is a race to win two games. Points for tricks bid and made go “below the line.” When a side’s below-the-line total reaches 100, that side wins a game and becomes vulnerable, which raises its bonuses and its penalties. Overtricks, slam bonuses, and penalties go “above the line.” The side that wins the rubber earns a bonus of 700 for a 2-0 win or 500 for a 2-1 win. Each side’s final score adds all its points above and below the line, plus any rubber bonus.

Running the Program

The ASDF system lists the source files in load order:

 1 (asdf:defsystem #:bridge
 2   :description "Contract Bridge AI — text-based bridge game with AI opponents"
 3   :author "Mark Watson"
 4   :license "Apache-2.0"
 5   :version "0.1.0"
 6   :serial t
 7   :components ((:file "package")
 8                (:file "cards")
 9                (:file "bidding")
10                (:file "display")
11                (:file "play")
12                (:file "scoring")
13                (:file "engine")))

Load the system and start a game from the REPL:

1 (asdf:load-system :bridge)
2 (bridge:play-bridge)   ; play a full rubber as South
3 (bridge:auto-play)     ; one deal with four AI players

At a bid prompt you type 1H, 3NT, PASS (or P), DBL (or X), or RDBL (or XX). At a card prompt you type a card name such as AS, KH, or 10C, or the number of a card in the list of legal plays. Type Q at any prompt to quit.

Here is the output of one call to auto-play. The program first prints all four hands with their HCP and shape (spades, hearts, diamonds, clubs), then the auction, then each trick in play order with the trick’s winner:

 1 ═══ AUTO-PLAY GAME ═══
 2 
 3 All hands:
 4 
 5 North:
 6   ♠: A Q 6 3
 7   ♥: A 4
 8   ♦: K J 7 5
 9   ♣: Q 9 5
10   HCP: 16  Shape: 4-2-4-3-  Total: 16
11 
12 East:
13   ♠: K 2
14   ♥: 5 3
15   ♦: A 10 9 6
16   ♣: J 10 8 7 3
17   HCP: 8  Shape: 2-2-4-5-  Total: 9
18 
19 South:
20   ♠: J 9 8 5 4
21   ♥: K 10 8
22   ♦: Q
23   ♣: A K 6 4
24   HCP: 13  Shape: 5-3-1-4-  Total: 14
25 
26 West:
27   ♠: 10 7
28   ♥: Q J 9 7 6 2
29   ♦: 8 4 3 2
30   ♣: 2
31   HCP: 3  Shape: 2-6-4-1-  Total: 5
32   North: 1NT
33   East: Pass
34   South: 2♣
35   West: Pass
36   North: 2♠
37   East: Pass
38   South: Pass
39   West: Pass
40 
41   Contract: 2 Spades by North
42   Trick  1: 7♣ K♣ 2♣ 5♣ → South
43   Trick  2: J♠ 7♠ Q♠ K♠ → East
44   Trick  3: 3♣ 4♣ 10♠ 9♣ → West
45   Trick  4: 7♥ A♥ 3♥ 8♥ → North
46   Trick  5: A♠ 2♠ 4♠ 2♦ → North
47   Trick  6: 5♦ 6♦ Q♦ 3♦ → South
48   Trick  7: 9♠ 2♥ 3♠ 5♥ → South
49   Trick  8: 8♠ 4♦ 6♠ 8♣ → South
50   Trick  9: 5♠ 6♥ 4♥ 9♦ → South
51   Trick 10: K♥ 9♥ 7♦ 10♣ → South
52   Trick 11: 10♥ J♥ J♦ 10♦ → West
53   Trick 12: 8♦ K♦ A♦ 6♣ → East
54   Trick 13: J♣ A♣ Q♥ Q♣ → South
55 
56   N-S: 9 tricks, E-W: 4 tricks
57   Score: 60

North holds a balanced 16 HCP and opens 1NT. South has 13 HCP and a five-card spade suit, so South bids 2♣, the Stayman convention, to ask North for a four-card major. North shows four spades with 2♠. North-South make nine tricks, one more than the eight they need. The 60 points for two spade tricks go below the line. We return to this auction later, because South should have bid on to game.

Architecture

The code has six layers. Each file depends only on files that load before it:

Architecture of the rubber bridge program
File Role
package.lisp Defines the bridge package and its exports
cards.lisp Cards, the deck, shuffle and deal, hand analysis
bidding.lisp Bids, auction rules, and the bidding AI
display.lisp Text rendering of hands, the table, and the auction
play.lisp Legal plays, trick winners, the card play AI, Monte Carlo play
scoring.lisp Rubber bridge scoring and the scorecard
engine.lisp Game state, human input, the bidding, play, and scoring phases

Cards and Hands

A card is a small struct with an integer suit from 0 to 3 and an integer rank from 2 to 14, where 14 is the Ace. Constants name the suits, and +notrump+ (4) extends the suit range for bids:

 1 (defconstant +clubs+    0)
 2 (defconstant +diamonds+ 1)
 3 (defconstant +hearts+   2)
 4 (defconstant +spades+   3)
 5 (defconstant +notrump+  4)  ; Used in bidding, not in cards
 6 
 7 (defstruct (card (:constructor make-card (suit rank))
 8                  (:print-function print-card))
 9   "A playing card with suit (0-3) and rank (2-14)."
10   (suit 0 :type fixnum)
11   (rank 2 :type fixnum))

Since suits are ordered integers, the code compares bids and finds trumps with plain arithmetic. Players are also integers: North 0, East 1, South 2, West 3. So (mod player 2) gives the team (0 for North-South) and (mod (+ player 2) 4) gives the partner.

make-deck builds a 52-card vector and shuffle-deck runs a Fisher-Yates shuffle on it in place. A hand is a list of cards. The hand analysis functions compute what the bidding AI needs:

 1 (defun hand-hcp (hand)
 2   "Total high card points in a hand."
 3   (reduce #'+ hand :key #'card-hcp))
 4 
 5 (defun hand-total-points (hand &optional (trump-fit-found nil))
 6   "Total points = HCP + distributional points.
 7    Before trump fit: use length points.
 8    After trump fit: use shortness points instead."
 9   (+ (hand-hcp hand)
10      (if trump-fit-found
11          (hand-shortness-points hand)
12          (hand-length-points hand))))
13 
14 (defun hand-balanced-p (hand)
15   "A balanced hand has shape 4-3-3-3, 4-4-3-2, or 5-3-3-2."
16   (let ((shape (hand-shape-sorted hand)))
17     (or (equal shape '(4 3 3 3))
18         (equal shape '(4 4 3 2))
19         (equal shape '(5 3 3 2)))))

Other helpers in cards.lisp find suit lengths, the longest suit, HCP in one suit, and stoppers (a card that will stop the opponents from running a suit in a no trump contract).

The Bidding AI

The bidding AI uses a simplified form of Standard American, the most common bidding system in North America. It is a rule-based decision tree. Each rule tests the hand’s HCP, its shape, and the bids made so far. The file BRIDGE_BIDDING_DECISIONS.md lists the rules in plain English, and the docstrings in bidding.lisp point back to it.

Bids and the Auction

A bid is a struct with a type (:suit, :pass, :double, or :redouble), a level, and a suit. To compare two suit bids, bid-index maps them onto one scale, from 1♣ at 0 up to 7NT at 34:

1 (defun bid-index (bid)
2   "Numeric index for comparing bids. 1♣=0, 1♦=1, ..., 7NT=34."
3   (when (bid-suit-p bid)
4     (+ (* (1- (bid-level bid)) 5)
5        (bid-suit bid))))

The auction history is a list with the newest bid first. To find who made a given bid, the code counts back from the dealer: the bid at index i came from player (mod (+ dealer (- (length bid-history) 1 i)) 4).

extract-contract turns a finished auction into a contract. It walks the history from newest to oldest until it reaches the last suit bid, and it records any double or redouble it passes on the way. A double that comes before the last suit bid does not count, because the later bid cancelled it. Then it walks from oldest to newest to find the first player on the declaring side who bid the contract’s suit. That player becomes declarer:

 1 (defun extract-contract (bid-history dealer)
 2   "Extract the final contract from the bidding.
 3    Returns (values contract-bid declarer doubled-state).
 4    doubled-state: 0=undoubled, 1=doubled, 2=redoubled."
 5   (let ((final-bid nil)
 6         (final-bidder nil)
 7         (doubled 0))
 8     (loop for i from 0 below (length bid-history)
 9           for bid = (nth i bid-history)
10           do (cond
11                ((bid-suit-p bid)
12                 (setf final-bid bid
13                       final-bidder (mod (+ dealer (- (length bid-history) 1 i)) 4))
14                 (return))
15                ((bid-redouble-p bid)
16                 (setf doubled 2))
17                ((bid-double-p bid)
18                 (setf doubled (max doubled 1)))))
19     (when final-bid
20       (let* ((declaring-side (mod final-bidder 2))
21              (contract-suit (bid-suit final-bid))
22              (declarer final-bidder))
23         (loop for i from (1- (length bid-history)) downto 0
24               for bid = (nth i bid-history)
25               for bidder = (mod (+ dealer (- (length bid-history) 1 i)) 4)
26               do (when (and (bid-suit-p bid)
27                             (= (bid-suit bid) contract-suit)
28                             (= (mod bidder 2) declaring-side))
29                    (setf declarer bidder)
30                    (return)))
31         (values final-bid declarer doubled)))))

In the sample deal North bid spades first, so North became declarer even though South made the Stayman bid.

The Top-Level Bidding Function

ai-select-bid decides which of three cases applies and calls the matching function:

  1. ai-opening-bid when no one has made a suit bid yet.
  2. ai-responding-bid when partner has bid and we have not.
  3. ai-opener-rebid when both we and partner have bid.

If only the opponents have bid, the AI makes a simple overcall: one of a five-card major with 13+ HCP, or 1NT with a balanced 15 to 18 HCP:

 1 (defun ai-select-bid (hand bid-history my-position dealer)
 2   "Main AI bidding entry point.
 3    Determines whether to open, respond, rebid, or pass.
 4    Implements Standard American with slam conventions from
 5    BRIDGE_BIDDING_DECISIONS.md."
 6   (let* ((current-highest (last-suit-bid bid-history))
 7          (partner-bid (find-partner-opening bid-history my-position dealer))
 8          (my-bid (find-my-opening bid-history my-position dealer))
 9          (raw-bid
10            (cond
11              ;; I already bid and partner responded — opener's rebid
12              ((and my-bid partner-bid)
13               (ai-opener-rebid hand my-bid partner-bid))
14              ;; Partner has bid but I haven't — respond
15              (partner-bid
16               (ai-responding-bid hand partner-bid bid-history my-position dealer))
17              ;; No one has bid a suit yet — try to open
18              ((null current-highest)
19               (ai-opening-bid hand))
20              ;; Opponents have bid, partner hasn't — overcall (simplified)
21              (t
22               (let ((hcp (hand-hcp hand)))
23                 (cond
24                   ((and (>= hcp 13)
25                         (hand-has-major-p hand 5))
26                    (if (>= (hand-suit-length hand +spades+) 5)
27                        (make-bid 1 +spades+)
28                        (make-bid 1 +hearts+)))
29                   ((and (>= hcp 15) (<= hcp 18)
30                         (hand-balanced-p hand))
31                    (make-bid 1 +notrump+))
32                   (t nil)))))))
33     ;; Ensure the bid is legal
34     (let ((legal-bid (ensure-legal-bid raw-bid current-highest)))
35       (or legal-bid (make-pass)))))

The rule functions return nil to mean pass. They also do not check that a bid tops the current one. ensure-legal-bid does that last check and turns any bid that is too low into a pass. This keeps each rule short.

Opening Bids

ai-opening-bid follows the Standard American order. With 22+ HCP it opens 2♣, an artificial bid that forces partner to reply. A balanced hand opens 2NT with 20 or 21 HCP and 1NT with 15 to 17. Other hands with 13 to 21 HCP open one of a suit, with a five-card major ahead of a minor. Below 13 HCP, a seven-card suit with two honors opens at the three level, and a six-card suit other than clubs opens a weak two:

 1 (defun ai-opening-bid (hand)
 2   "Determine AI's opening bid using Standard American conventions.
 3    Rules from BRIDGE_BIDDING_DECISIONS.md."
 4   (let ((hcp (hand-hcp hand))
 5         (balanced (hand-balanced-p hand)))
 6     (cond
 7       ;; 2♣ — Very strong hand (22+ HCP), artificial forcing
 8       ((>= hcp 22)
 9        (make-bid 2 +clubs+))
10 
11       ;; 2NT — Balanced, 20-21 HCP
12       ((and balanced (>= hcp 20) (<= hcp 21))
13        (make-bid 2 +notrump+))
14 
15       ;; 1NT — Balanced, 15-17 HCP (no singleton, good doubleton OK)
16       ((and balanced (>= hcp 15) (<= hcp 17))
17        (make-bid 1 +notrump+))
18 
19       ;; 1 of a suit — 13-21 HCP
20       ((and (>= hcp 13) (<= hcp 21))
21        (cond
22          ;; Both 5-card majors: bid the higher-ranking (spades)
23          ((and (>= (hand-suit-length hand +spades+) 5)
24                (>= (hand-suit-length hand +hearts+) 5))
25           (make-bid 1 +spades+))
26          ;; 5+ card major — bid it
27          ((>= (hand-suit-length hand +spades+) 5)
28           (make-bid 1 +spades+))
29          ((>= (hand-suit-length hand +hearts+) 5)
30           (make-bid 1 +hearts+))
31          ;; Minor: 3+ cards, longer minor wins
32          ((> (hand-suit-length hand +diamonds+)
33              (hand-suit-length hand +clubs+))
34           (make-bid 1 +diamonds+))
35          ;; Equal minors or longer clubs
36          (t (make-bid 1 +clubs+))))
37 
38       ;; Preemptive 3-level — good 7-card suit with 2+ honors, sub-opening
39       ((and (< hcp 13)
40             (let ((seven-card-suit
41                     (loop for suit in +suits+
42                           when (and (>= (hand-suit-length hand suit) 7)
43                                     (>= (hand-suit-honor-count hand suit) 2))
44                           return suit)))
45               seven-card-suit))
46        (let ((seven-card-suit
47                (loop for suit in +suits+
48                      when (and (>= (hand-suit-length hand suit) 7)
49                                (>= (hand-suit-honor-count hand suit) 2))
50                      return suit)))
51          (make-bid 3 seven-card-suit)))
52 
53       ;; Weak twos — Less than opening hand, 6-card suit (not clubs)
54       ((and (< hcp 13) (>= hcp 5))
55        (let ((six-card-suit
56                (loop for suit in (list +spades+ +hearts+ +diamonds+)
57                      when (>= (hand-suit-length hand suit) 6)
58                      return suit)))
59          (when six-card-suit
60            (make-bid 2 six-card-suit))))
61 
62       ;; Pass
63       (t nil))))

Responses

ai-responding-bid branches on partner’s opening: 1NT, 2NT, one of a major, one of a minor, 2♣, a weak two, or a three-level preempt. Each branch then picks a bid by HCP and suit lengths. For example, over partner’s 1NT the AI:

  • passes, or signs off in a five-card suit at the two level, with 0 to 7 HCP;
  • bids 2♣ (Stayman) with 8+ HCP and a four-card major;
  • bids 2NT, inviting game, with 8 or 9 HCP;
  • with 10 to 15 HCP, bids game in a six-card major, bids three of a five-card major to invite, or else bids 3NT;
  • bids 4NT, a slam try, with 16+ HCP.

Over a one-level minor opening the AI looks first for a four-card major, then for a no trump bid, and last for a raise of partner’s minor.

Rebids and Slam Conventions

ai-opener-rebid first checks for three conventions that ask a question:

  • Stayman (2♣ over 1NT, or 3♣ over 2NT) asks for a four-card major. Opener bids hearts if they hold four, then spades, and else diamonds to deny a major.
  • Blackwood (4NT after a suit bid) asks for aces. Blackwood 5NT then asks for kings.
  • Gerber (4♣ after a no trump bid) asks for aces, and 5♣ then asks for kings.

The Blackwood reply needs no table. The reply suits 5♣, 5♦, 5♥, 5♠, 5NT stand for 0 to 4 aces, and the suit integers run from 0 to 4 in that same order, so the ace count gives the suit. Since a hand holds at most four aces, (mod aces 5) just returns aces:

1       ;; ── Blackwood 4NT: respond with ace count ──
2       ((blackwood-response-p partner-response)
3        (let ((aces (hand-ace-count hand)))
4          ;; 5♣=0, 5♦=1, 5♥=2, 5♠=3, 5NT=4
5          (make-bid 5 (mod aces 5))))

Gerber replies start at 4♦ and run past 4NT to 5♣, so that branch spells out each case:

 1       ;; ── Gerber 4♣ (after NT bids): respond with ace count ──
 2       ((gerber-response-p partner-response my-opening)
 3        (let ((aces (hand-ace-count hand)))
 4          ;; 4♦=0, 4♥=1, 4♠=2, 4NT=3, 5♣=4
 5          (case aces
 6            (0 (make-bid 4 +diamonds+))
 7            (1 (make-bid 4 +hearts+))
 8            (2 (make-bid 4 +spades+))
 9            (3 (make-bid 4 +notrump+))
10            (4 (make-bid 5 +clubs+)))))

If none of the conventions apply, opener raises partner’s no trump with extra values, bids game or tries for slam after partner raises, or supports partner’s new suit.

A Weakness in the Sample Auction

The sample deal shows a limit of this design. find-partner-opening and find-my-opening each return a player’s first suit bid. When South must bid after North’s 2♠, ai-select-bid sees South’s 2♣ as “my bid” and North’s 1NT as “partner’s bid,” so it calls ai-opener-rebid as if South had opened 2♣ and North had replied 1NT. The no trump branch passes with 15 HCP or less. South never sees North’s 2♠, so it passes with a nine-card spade fit and 29 combined HCP, enough to bid a 4♠ game. A better design would pass the whole auction to the rebid logic. That is a good exercise for the reader.

The Card Play AI

Rules of Play

legal-plays returns every card in hand when leading, the cards in the suit led when the player can follow, and every card when they can not. trick-winner takes the four cards of a trick in play order and the index of the player who led:

 1 (defun trick-winner (trick lead-player trump-suit)
 2   "Determine the winner of a completed trick.
 3    trick is a list of 4 cards in play order.
 4    Returns the winning player index."
 5   (let* ((lead-card (first trick))
 6          (lead-suit (card-suit lead-card))
 7          (best-card lead-card)
 8          (best-player lead-player))
 9     (loop for i from 1 below 4
10           for card = (nth i trick)
11           for player = (mod (+ lead-player i) 4)
12           do (cond
13                ;; Trump beats non-trump
14                ((and trump-suit
15                      (= (card-suit card) trump-suit)
16                      (/= (card-suit best-card) trump-suit))
17                 (setf best-card card
18                       best-player player))
19                ;; Higher trump beats lower trump
20                ((and trump-suit
21                      (= (card-suit card) trump-suit)
22                      (= (card-suit best-card) trump-suit)
23                      (> (card-rank card) (card-rank best-card)))
24                 (setf best-card card
25                       best-player player))
26                ;; Higher card in lead suit (when best is also lead suit)
27                ((and (= (card-suit card) lead-suit)
28                      (= (card-suit best-card) lead-suit)
29                      (> (card-rank card) (card-rank best-card)))
30                 (setf best-card card
31                       best-player player))
32                ;; Otherwise: off-suit non-trump card loses
33                ))
34     best-player))

trick-winner-partial does the same for a trick with fewer than four cards. The AI uses it to find out who is winning the trick so far.

Leading

ai-select-card calls ai-select-lead when the trick is empty and ai-select-follow otherwise. A defender tries a list of standard leads and takes the first that applies:

  1. Top of a sequence: lead the King from K-Q-J, which tells partner you hold the Queen.
  2. Fourth best: lead the fourth-highest card of the longest side suit.
  3. Short suit: in a trump contract, lead the top card of the shortest side suit, hoping to ruff that suit later.
  4. Else lead the lowest card of the longest side suit.

Declarer draws trumps first if they hold three or more with the Queen or better, then tries a sequence lead and then fourth best. In the sample deal East led the 7♣ from J-10-8-7-3. The J-10-8 is not a sequence, so the AI chose the fourth-best card.

Following

When following, the AI applies a few classic rules: play low when partner is winning in fourth seat, win as cheaply as possible, and play low when it can not win:

 1 (defun ai-select-follow (hand trick lead-player lead-suit trump-suit
 2                           my-position declarer)
 3   "AI selects a card when following (not leading).
 4    Uses heuristic play strategies."
 5   (let* ((legal (legal-plays hand lead-suit))
 6          (my-side (mod my-position 2))
 7          (decl-side (mod declarer 2))
 8          (defending (not (= my-side decl-side)))
 9          (partner-pos (partner my-position))
10          (cards-played (length trick))
11          ;; Who's winning so far?
12          (current-winner (when (> cards-played 0)
13                            (trick-winner-partial trick lead-player trump-suit)))
14          (partner-winning (and current-winner
15                                (= (mod current-winner 2) my-side))))
16     (declare (ignore partner-pos))
17 
18     (cond
19       ;; Only one legal play
20       ((= (length legal) 1)
21        (first legal))
22 
23       ;; MUST follow suit
24       ((some (lambda (c) (= (card-suit c) lead-suit)) legal)
25        (let ((in-suit (remove-if-not (lambda (c) (= (card-suit c) lead-suit)) legal)))
26          (cond
27            ;; Last to play (4th seat) — play as cheaply as possible if partner winning
28            ((and (= cards-played 3) partner-winning)
29             (lowest-card in-suit))
30            ;; Can win the trick — play just enough to win
31            ((can-win-trick in-suit trick lead-player trump-suit)
32             (cheapest-winner in-suit trick lead-player trump-suit))
33            ;; Can't win — play lowest
34            (t (lowest-card in-suit)))))
35 
36       ;; CAN'T follow suit — can trump or discard
37       (t
38        (let* ((trumps (when trump-suit
39                         (remove-if-not (lambda (c) (= (card-suit c) trump-suit)) legal)))
40               (non-trumps (if trump-suit
41                               (remove-if (lambda (c) (= (card-suit c) trump-suit)) legal)
42                               legal))
43               ;; Only trumps that beat the current winner (which may itself
44               ;; be a trump) actually win the trick
45               (winner (when trick (current-winning-card trick lead-player trump-suit)))
46               (winning-trumps
47                 (remove-if-not (lambda (c)
48                                  (or (null winner)
49                                      (/= (card-suit winner) trump-suit)
50                                      (> (card-rank c) (card-rank winner))))
51                                trumps)))
52          (cond
53            ;; Defending: trump if possible and partner isn't winning
54            ((and defending winning-trumps (not partner-winning))
55             (lowest-card winning-trumps)) ; Use lowest trump that wins
56            ;; Declaring: trump when beneficial
57            ((and (not defending) winning-trumps (not partner-winning))
58             (lowest-card winning-trumps))
59            ;; Discard — throw away lowest card from weakest suit
60            ;; (or the lowest trump when holding nothing but trumps)
61            (t (or (best-discard non-trumps trump-suit)
62                   (lowest-card trumps)))))))))

A card that follows suit can win only if it outranks the card now winning, and only if that card is in the same suit. Once someone ruffs, no card in the suit led can win, however high. beats-current-winner-p makes this test, and both can-win-trick and cheapest-winner call it:

 1 (defun current-winning-card (trick lead-player trump-suit)
 2   "The card currently winning the partial TRICK."
 3   (nth (mod (- (trick-winner-partial trick lead-player trump-suit) lead-player) 4)
 4        trick))
 5 
 6 (defun beats-current-winner-p (card trick lead-player trump-suit)
 7   "Would CARD, which follows the lead suit, beat the current winner of TRICK?
 8    It can only win if the trick has not been trumped (or the lead suit is
 9    trump) and it outranks the winning card."
10   (let ((winner (current-winning-card trick lead-player trump-suit)))
11     (and (= (card-suit card) (card-suit winner))
12          (> (card-rank card) (card-rank winner)))))

One detail needs care. The game engine builds each trick with push, so its list holds the newest card first. The play AI expects play order, with the lead card first, so the engine passes (reverse trick) to ai-select-card. With the wrong order, trick-winner-partial would treat the last card played as the lead.

Monte Carlo Card Play

play.lisp also holds a prototype of Perfect Information Monte Carlo (PIMC) play. The AI can not see the other hands, so it guesses them many times and averages the results:

  1. Deal the unseen cards at random to the other three players, giving each the right number of cards.
  2. For each legal card, play it and then play out the rest of the deal with the heuristic AI in all four seats.
  3. Choose the card with the highest average number of tricks for our side.
 1 (defun monte-carlo-select-card (hand trick lead-player lead-suit trump-suit
 2                                 my-position declarer ns-tricks ew-tricks
 3                                 cards-played &key (num-samples 50))
 4   "Use Monte Carlo simulation to select the best card.
 5    Tries each legal play and simulates NUM-SAMPLES random completions."
 6   (let* ((legal (if lead-suit
 7                     (legal-plays hand lead-suit)
 8                     (copy-list hand)))
 9          (all-cards (make-deck))
10          ;; Cards we've seen: our hand + cards already played
11          (seen-cards (append (copy-list hand)
12                              (copy-list cards-played)
13                              (when trick (copy-list trick))))
14          (remaining (remove-if (lambda (c)
15                                  (some (lambda (s) (card= c s)) seen-cards))
16                                (coerce all-cards 'list)))
17          (other-sizes (make-array 4 :initial-element 0))
18          (best-card (first legal))
19          (best-score most-negative-fixnum))
20     ...
21     (dolist (card legal)
22       (let ((total-score 0))
23         (dotimes (sample num-samples)
24           (let* ((sim-hands (generate-consistent-deal
25                              (remove card hand :test #'card= :count 1)
26                              my-position remaining other-sizes))
27                  ...)
28             ...
29             (let ((result (simulate-remaining-tricks
30                            sim-hands
31                            (or next-leader (mod (+ my-position 1) 4))
32                            trump-suit declarer sim-ns sim-ew)))
33               ;; Score from our perspective
34               (if (evenp my-position)
35                   (incf total-score (car result))   ; N-S tricks
36                   (incf total-score (cdr result)))))) ; E-W tricks
37 
38         ;; Average score for this card
39         (let ((avg-score (/ total-score num-samples)))
40           (when (> avg-score best-score)
41             (setf best-score avg-score
42                   best-card card)))))
43     best-card))

The cost grows with the number of legal cards times num-samples times the number of tricks left. The random deals use only the cards seen so far. They ignore what the auction revealed, such as a 1NT opener’s balanced shape. The game loop does not call monte-carlo-select-card yet; ai-select-card uses only the heuristics. You can call it from the REPL to compare its choices with the heuristic player. Matthew Ginsberg’s GIB program used this PIMC approach, with a fast double dummy solver in place of heuristic playouts, and became the first bridge program to play at expert level.

Scoring

Rubber bridge scoring has many special cases. scoring.lisp keeps the state of a rubber in one struct:

 1 (defstruct rubber-state
 2   "State of an ongoing rubber bridge match."
 3   ;; Below the line: trick points toward game
 4   (ns-below      0)    ; N-S points below the line (current game)
 5   (ew-below      0)    ; E-W points below the line (current game)
 6   ;; Below-the-line points over the whole rubber; NS-BELOW / EW-BELOW are
 7   ;; reset after each game but every trick point counts in the final total
 8   (ns-below-total 0)
 9   (ew-below-total 0)
10   ;; Above the line: bonuses and penalties
11   (ns-above      0)    ; N-S points above the line (cumulative)
12   (ew-above      0)    ; E-W points above the line (cumulative)
13   ;; Games won
14   (ns-games      0)    ; N-S games won in this rubber
15   (ew-games      0)    ; E-W games won in this rubber
16   ;; Derived vulnerability (updated after each game)
17   (ns-vulnerable nil)
18   (ew-vulnerable nil)
19   ;; Dealer rotates each deal
20   (current-dealer +north+)
21   ;; History of deals played
22   (deals-played   0))

Trick Values

Each trick bid and made in clubs or diamonds is worth 20 points, and each in hearts, spades, or no trump is worth 30. The first no trump trick earns 10 more. A double multiplies trick values by 2 and a redouble by 4:

 1 (defun trick-value (suit &optional (doubled 0))
 2   "Base value per trick for a given strain.
 3    Clubs/Diamonds = 20, Hearts/Spades = 30, NT = 30 (40 for first)."
 4   (let ((base (cond
 5                 ((<= suit +diamonds+) 20)  ; Minors
 6                 (t 30))))                   ; Majors and NT
 7     (case doubled
 8       (1 (* base 2))
 9       (2 (* base 4))
10       (t base))))

contract-trick-score adds the extra no trump points. A side wins a game when its trick score reaches 100, so the lowest undoubled game contracts are 3NT (Code Test), 4♥ or 4♠ (Code Test), and 5♣ or 5♦ (Code Test).

Scoring a Deal

score-rubber-deal takes the contract, the tricks declarer won, and the rubber state, and updates the state in place. When declarer makes the contract, the trick score goes below the line, and overtricks, the “insult” bonus for making a doubled contract, and slam bonuses go above it. A grand slam earns its own bonus in place of the small slam bonus, so the code uses one cond. Two when forms would award both, since slam-p is true at levels 6 and 7:

1           ;; Slam bonus (above the line): a grand slam earns the grand slam
2           ;; bonus instead of (not in addition to) the small slam bonus
3           (cond ((grand-slam-p level)
4                  (incf above-score (if vul 1500 1000)))
5                 ((slam-p level)
6                  (incf above-score (if vul 750 500))))

When a side reaches 100 below the line, it wins a game and becomes vulnerable, and both sides’ below-the-line scores for the current game reset to zero. The slots ns-below-total and ew-below-total keep running totals over the whole rubber, because the final score counts every trick point, not only those in the current game.

When declarer fails, the defenders score a penalty above the line for each trick short (“down”). Undoubled, each down trick costs 50, or 100 when vulnerable. Doubled penalties grow faster. Not vulnerable they run 100, 300, 500, and then 300 more for each extra trick. Vulnerable they run 200 and then 300 more for each extra trick. Redoubled penalties are twice the doubled ones:

 1         (let* ((down (abs overtricks))
 2                (penalty
 3                  (case doubled
 4                    ;; Undoubled penalties
 5                    (0 (* down (if vul 100 50)))
 6                    ;; Doubled penalties
 7                    (1 (if vul
 8                           (+ 200 (* (max 0 (1- down)) 300))
 9                           (cond
10                             ((= down 1) 100)
11                             ((= down 2) 300)
12                             ((= down 3) 500)
13                             (t (+ 500 (* (- down 3) 300))))))
14                    ;; Redoubled
15                    (2 (if vul
16                           (* 2 (+ 200 (* (max 0 (1- down)) 300)))
17                           (* 2 (cond
18                                  ((= down 1) 100)
19                                  ((= down 2) 300)
20                                  ((= down 3) 500)
21                                  (t (+ 500 (* (- down 3) 300))))))))))
22 
23           ;; Penalties go above the line for the defenders
24           (if ns-side
25               (incf (rubber-state-ew-above rubber-state) penalty)
26               (incf (rubber-state-ns-above rubber-state) penalty))
27 
28           (- penalty))

The function returns the below-the-line score for a made contract and the penalty as a negative number for a failed one.

Here is a short REPL test. South makes 4♥ with one overtrick, which wins a game. Then East goes down one in 2♠:

1 (in-package :bridge)
2 (let ((rs (make-rubber-state)))
3   (score-rubber-deal 4 +hearts+ 11 +south+ 0 rs)
4   (score-rubber-deal 2 +spades+ 7 +east+ 0 rs)
5   (display-rubber-score rs)
6   (multiple-value-list (rubber-total-scores rs)))

The scorecard shows North-South with one game, vulnerable, and 80 points above the line: 30 for the overtrick and 50 for East’s down trick. Their 120 points below the line moved to the rubber total when they won the game:

 1 ╔════════════════════╦════════════════════╗
 2 ║   RUBBER BRIDGE SCORECARD              ║
 3 ╠════════════════════╬════════════════════╣
 4 ║      N-S           ║      E-W           ║
 5 ╠════════════════════╬════════════════════╣
 6 ║  Above:    80       ║  Above:     0       ║
 7 ╠────────────────────╬────────────────────╣
 8 ║  Below:     0       ║  Below:     0       ║
 9 ║  Games: 1         ║  Games: 0         ║
10 ║  Vul:   YES       ║  Vul:   No        ║
11 ╚════════════════════╩════════════════════╝

rubber-total-scores returns (200 0): 80 above the line plus 120 below for North-South. No one has won the rubber yet, so there is no rubber bonus.

The Game Engine

Game State

engine.lisp keeps everything about one deal in a single struct that each phase reads and updates:

 1 (defstruct game-state
 2   "Complete state of a bridge game."
 3   (hands          nil)   ; Array of 4 hands (lists of cards)
 4   (original-hands nil)   ; Copy of initial deal for review
 5   (dealer         0)     ; Who dealt (0=N, 1=E, 2=S, 3=W)
 6   (vulnerability  :none) ; :none, :ns, :ew, :both
 7   (human-player   +south+)
 8   ;; Bidding
 9   (bid-history    nil)   ; List of bids (most recent first)
10   (contract       nil)   ; Final contract bid
11   (declarer       nil)   ; Declarer player index
12   (doubled        0)     ; 0, 1, or 2
13   ;; Play
14   (dummy          nil)   ; Dummy player index
15   (current-trick  nil)   ; Cards played so far in current trick
16   (trick-lead     nil)   ; Who leads current trick
17   (tricks-ns      0)     ; Tricks won by N-S
18   (tricks-ew      0)     ; Tricks won by E-W
19   (tricks-played  0)     ; Total tricks completed
20   (cards-played   nil)   ; All cards played so far
21   (trump-suit     nil)   ; Trump suit (nil = NT)
22   ;; Phase
23   (phase          :dealing)) ; :dealing, :bidding, :playing, :scoring, :done

The Three Phases

run-bidding-phase asks each seat for a bid in turn: the human at South through prompt-for-bid, and the AI elsewhere through ai-select-bid. It stops after three passes that follow a suit bid, or four passes if no one opened. Then it calls extract-contract and sets the declarer, dummy, trump suit, and opening leader.

The human always sits South. If North wins the contract, South would be dummy and have nothing to do. To avoid that, the engine swaps the North and South hands, so the human plays declarer from the South seat and North holds the dummy cards:

1           (when (and (= (game-state-human-player gs) +south+)
2                      (= declarer +north+))
3             (let ((north-cards (aref (game-state-hands gs) +north+))
4                   (south-cards (aref (game-state-hands gs) +south+)))
5               (setf (aref (game-state-hands gs) +south+) north-cards
6                     (aref (game-state-hands gs) +north+) south-cards))
7             ;; Swap dummy designation: North now holds the dummy cards
8             (setf (game-state-dummy gs) +north+)
9             ...)

run-playing-phase plays 13 tricks. For each card it decides whether the human or the AI plays it. The human plays their own hand and also plays dummy when North-South declare. run-scoring-phase finds the tricks declarer’s side won, calls score-rubber-deal, and prints the deal result and the scorecard.

The Rubber Loop

play-bridge runs a whole rubber. It deals, bids, plays, and scores deal after deal, passing the dealer clockwise, until one side wins two games:

 1 (defun play-bridge ()
 2   "Play a complete rubber of bridge (first to 2 games wins).
 3    Type Q or QUIT at any prompt to exit."
 4   (display-welcome)
 5 
 6   (let ((rs (make-rubber-state)))
 7     (catch 'quit-game
 8       (loop while (not (rubber-complete-p rs))
 9             do (let* ((dealer (rubber-state-current-dealer rs))
10                       (vulnerability (rubber-vulnerability rs))
11                       (gs (new-game :dealer dealer
12                                     :vulnerability vulnerability
13                                     :human-player +south+)))
14 
15                  (incf (rubber-state-deals-played rs))
16                  (format t "~%~%══════════════════════════════════════════~%")
17                  (format t "  DEAL #~D   (N-S games: ~D  E-W games: ~D)~%"
18                          (rubber-state-deals-played rs)
19                          (rubber-state-ns-games rs)
20                          (rubber-state-ew-games rs))
21                  (format t "══════════════════════════════════════════~%")
22 
23                  ;; Bidding
24                  (setf gs (run-bidding-phase gs))
25 
26                  ;; If passed out, skip to next deal
27                  (if (eq (game-state-phase gs) :done)
28                      (format t "~%  Deal passed out. Rotating dealer...~%")
29                      (progn
30                        ;; Playing
31                        (setf gs (run-playing-phase gs))
32                        ;; Scoring (updates rubber state)
33                        (run-scoring-phase gs rs)))
34 
35                  ;; Rotate dealer
36                  (setf (rubber-state-current-dealer rs)
37                        (next-player (rubber-state-current-dealer rs)))))
38 
39       ;; Rubber complete!
40       (display-rubber-final rs))
41     (format t "~%  Thanks for playing!~%")
42     rs))

The input prompts sit deep inside the bidding and play loops. When the user types Q, the prompt calls (throw 'quit-game nil), and control jumps straight to the catch in play-bridge. No function in between needs to check for a quit flag.

scoring.lisp runs (setf *random-state* (make-random-state t)) at load time, so each session deals new hands. To repeat a deal while testing, set *random-state* to a fixed seed before you call auto-play.

The Text UI

display.lisp draws the table as a compass, with North on top, West and East in the middle, and South at the bottom. It shows the human’s hand and the dummy in full, and shows only a card count for the other two hands. This is the board for the sample deal, from South’s seat with North as dummy:

 1 ════════════════════════════════════════════════════════════
 2                ♠ ♥ ♦ ♣  BRIDGE TABLE  ♣ ♦ ♥ ♠
 3                          Vul: NONE
 4 ────────────────────────────────────────────────────────────
 5                            NORTH
 6                          ♠ A Q 6 3
 7                            ♥ A 4
 8                          ♦ K J 7 5
 9                           ♣ Q 9 5
10 
11 WEST                                    EAST
12 [13 cards]                              [13 cards]
13 
14                         SOUTH (You)
15                         ♠ J 9 8 5 4
16                           ♥ K 10 8
17                             ♦ Q
18                          ♣ A K 6 4
19 ════════════════════════════════════════════════════════════

display-bidding-history prints the auction in four columns that line up under the dealer’s seat. The card prompt lists the legal plays with numbers, so you can type 2 in place of KH.

The input parsers accept several spellings. parse-bid-input maps P and PASS to a pass, D, DBL, DOUBLE, and X to a double, and R, RDBL, REDOUBLE, and XX to a redouble. Any other input must be a level from 1 to 7 followed by a strain such as H, HEARTS, ♥, N, or NT. parse-card-input first reads an all-digit string as an index into the legal plays, and else reads a rank followed by a suit letter.

Wrap Up

This program combines several AI techniques. The bidding module encodes Standard American as a decision tree. That works well because a bidding system is a formal protocol with written rules. The play module uses fast heuristics from bridge books, and it includes a Monte Carlo player that samples the hidden hands. The scoring module tracks the rubber bridge rules for the line, games, vulnerability, and bonuses.

Common Lisp suits this kind of program. Small structs with typed integer slots keep cards and bids cheap to create and compare, which matters when Monte Carlo play deals thousands of sample hands. The REPL lets you test one piece at a time: count a hand’s HCP, run one auction, or score one deal. catch and throw give the game loop a clean exit from deep inside the input code.

Here are some ways to extend the program:

  • Pass the full auction to the rebid logic. As the sample deal shows, the rebid code sees only each player’s first bid.
  • Competitive bidding. The overcall logic is minimal. A fuller version would add takeout doubles, Michaels cue bids, and the unusual 2NT.
  • Defensive signals. The AI does not yet signal attitude, count, or suit preference to its partner.
  • Use Monte Carlo play in the game. Call monte-carlo-select-card from ai-select-card for declarer, and bias the sample deals with what the auction revealed.
  • A double dummy solver. Replacing the heuristic playouts with an alpha-beta search over the visible sample hands would make each Monte Carlo sample far more accurate.