This is a fairly simple and yet effective double dummy solver for the card game of bridge. It's written in C++ and terminal based.
Try the web app, which runs the same solver compiled to WebAssembly. Its README covers its features, from DD tables, par and single-dummy shuffles to playing out deals and endings, and shareable links.
Requirement: Linux or macOS on x86-64 or ARM64, with G++ or Clang installed.
make
To build the web app, Emscripten is required. See web/README.md for version requirements.
make -C web
To check correctness in a few seconds:
make test
make -C web test
There are three ways to specify a deal to solve. Later sections write [DEAL]
for any one of them.
./solver -r
The output looks like below.
♠ KJT987 ♥ K5 ♦ 7 ♣ AQJ8
♠ 3 ♥ J9764 ♦ Q642 ♣ KT2 ♠ Q64 ♥ QT8 ♦ KJ953 ♣ 94
♠ A52 ♥ A32 ♦ AT8 ♣ 7653
N 13 13 0 0 0.00 s 5.3 M
S 13 13 0 0 0.01 s 5.3 M
H 7 7 6 5 0.07 s 7.9 M
D 6 6 6 6 0.09 s 7.9 M
C 13 13 0 0 0.09 s 7.9 M
Each line after the deal shows the strain to play, the number of tricks when South/North/West/East declares respectively, the cumulative time and the peak memory usage.
./solver -f [FILE]
The format of the deal in the file is like below.
KQ3 - T832 AJ9765
72 AJ972 AQ7 KQ2 T96 K83 654 T843
AJ854 QT654 KJ9 -
D
W
The first line is North. The second line has both West and East. The third line is South. The forth line specifies the strain to play. The fifth line is the leading seat. If the leading seat is not given, the deal is solved for all four leading seats. If the strain to play is also not given, the deal is solved for all five strains.
-i flag can be used to ignore the strain and the leading seat that are
specified in the file.
./solver -c [CODE]
Each bridge deal is encoded by West, North and East's card holdings. When -m
flag value has its lowest bit set, the solver outputs this code. For example,
./solver -r -m3
# 3019E0620005,100A3AD222,2C419DB
♠ J73 ♥ KJ9732 ♦ A62 ♣ 4
♠ AQ ♥ T65 ♦ JT9854 ♣ 98 ♠ KT8642 ♥ A4 ♦ K ♣ Q652
♠ 95 ♥ Q8 ♦ Q73 ♣ AKJT73
Then the code can be used to reproduce exactly the same deal like below.
./solver -c 3019E0620005,100A3AD222,2C419DB
./solver [DEAL] -t [STRAIN]
where [STRAIN] is one of {N, S, H, D, C}.
Solving a single deal with one thread per strain can be done with xargs:
echo N S H D C | xargs -n1 -P5 ./solver -if deals/freak/deal.0 -m0 -t
D 11 11 2 2 0.07 s 7.9 M
S 8 8 5 5 0.33 s 25.7 M
H 8 8 5 5 1.00 s 57.7 M
C 6 6 7 6 1.69 s 82.3 M
N 7 7 6 5 4.16 s 201.6 M
To get the strains sorted, pipe the previous command to
tr N Z | sort -r | tr Z N
./solver [DEAL] -p
The solver automatically determines the contract. If nobody can make any contract, the hand is skipped. For each turn, the solver evaluates each of the player's card and shows the result of the contract if the card is played and the rest is played by everyone optimally. A sign is shown next to each card with the following meanings.
| Sign | Meaning |
|---|---|
| = | The contract makes. |
| + | The contract gets an overtrick. |
| - | The contract is set by a trick. |
| (+N) | The contract gets N overtricks. |
| (-N) | The contract is set by N tricks. |
You can choose what card to play. For simplicity, only one of the equivalent cards like QJT in the same suit can be chosen. You can also undo the plays to explore all possibilities. Below is an example.
------ 3NT by NS: NS 0 EW 0 ------
N ♠ AK83
♥ AK
♦ A65432
21 ♣ K
W ♠ 65 E ♠ JT92
♥ QJT876 ♥ 54
♦ KT9 ♦ Q
11 ♣ AJ 3 ♣ 765432
S ♠ Q74
♥ 932
♦ J87
5 ♣ QT98
From ♠ 6+ ♥ Q=8= ♦ K(+2)T+ ♣ A+J+ West plays ♥ 8.
From ♥ A= North plays ♥ A.
From ♥ 5= East plays ♥ 5.
From ♥ 9=3= South plays ♥ 3.
------ 3NT by NS: NS 1 EW 0 ------
N ♠ AK83
♥ K
♦ A65432
17 ♣ K
W ♠ 65 E ♠ JT92
♥ QJT76 ♥ 4
♦ KT9 ♦ Q
11 ♣ AJ 3 ♣ 765432
S ♠ Q74
♥ 92
♦ J87
5 ♣ QT98
From ♠ A-8(-2)3(-2) ♥ K(-2) ♦ A-6(-2) ♣ K= North plays ♣ K?
./shuffle.py [DEAL] -n [ROUNDS] -j [PARALLELISM] -s [SEATS]
where [SEATS] is any combination of {W, N, E, S}, e.g. EW.
By default it shuffles one side's cards while holding the other side's cards fixed. In the example below, the first half is just the double-dummy result of the deal; the second half are percentages of getting certain number of tricks, according to double-dummy results of the shuffles.
# 801138827A000,858EA3208,1BCB0E2
N ♠ J52
♥ K42
♦ QJT2
12 ♣ AJ6
W ♠ - E ♠ K876
♥ AQJT96 ♥ 53
♦ K9874 ♦ 6
13 ♣ K2 5 ♣ QT9854
S ♠ AQT943
♥ 87
♦ A53
10 ♣ 73
N 9 9 3 3 0.01 s 6.0 M
S 10 9 3 3 0.03 s 6.3 M
H 4 4 8 8 0.05 s 6.3 M
D 7 7 5 6 0.10 s 11.7 M
C 6 6 6 6 0.12 s 11.7 M
S N 7S 7N 8S 8N 9S 9N 10S 10N 11S 11N 12S 12N 13S 13N
N 7.3 7.7 66 72 54 54 36 36 22 26 6 10 0 0 0 0
S 9.4 9.4 100 100 100 100 76 76 48 50 14 16 4 4 0 0
H 4.6 4.7 4 6 2 2 0 0 0 0 0 0 0 0 0 0
D 7.9 8.1 84 88 56 66 30 30 14 16 4 6 4 4 0 0
C 4.6 4.8 4 6 2 2 0 0 0 0 0 0 0 0 0 0
W E 7W 7E 8W 8E 9W 9E 10W 10E 11W 11E 12W 12E 13W 13E
N 4.3 4.4 4 6 0 0 0 0 0 0 0 0 0 0 0 0
S 3.5 3.6 0 0 0 0 0 0 0 0 0 0 0 0 0 0
H 8.0 7.8 98 96 80 68 26 20 0 0 0 0 0 0 0 0
D 5.6 5.5 16 16 2 0 0 0 0 0 0 0 0 0 0 0
C 7.4 7.5 82 80 48 52 18 20 0 2 0 0 0 0 0 0
Specifically, S 9.4 9.4 means either South or North averages 9.4 tricks
when declaring a spade contract. 48 50 on the same row shows South has 48%
chance of making 4♠ while North has a slightly higher chance at 50%.
Run one of the following commands to measure performance and check correctness.
The directory can be deals/fixed (the default), deals/old, deals/new,
deals/hard, deals/long, deals/1k or deals/freak. For parallel runs,
the number of threads is 2 by default.
./run.sh [DIRECTORY]
./parallel_run.sh [DIRECTORY] [THREADS]
./parallel_run_strain.sh [DIRECTORY] [THREADS]
parallel_run_strain.sh parallelizes one thread per deal-strain pair instead of
one thread per deal, useful for directories with few but hard deals (e.g.
deals/freak) where per-deal parallelism alone can't use more threads than
there are deals.
To solve random deals instead of deals in a directory:
./parallel_run_random.sh [COUNT] [THREADS]
make perf runs a benchmark suite with the setup below, taking a few minutes.
PERF_CPU picks the CPU to pin to and PERF_SETS the directories.
Benchmarks below run on AMD Ryzen 7 5800H
with 8 physical cores at 3.2GHz base clock and 4.4GHz boost clock. To get stable performance
numbers, all irrelevant applications are closed and taskset is used to bind the process
to a single core for single-core runs.
Also GLIBC_TUNABLES=glibc.malloc.hugetlb=1 is set to enable transparent huge
pages with glibc 2.35 or later. (/sys/kernel/mm/transparent_hugepage/enabled
must be set to madvise or always.) Fewer TLB misses make the solver about
2% faster on typical deals and up to 10% faster on very difficult deals, at the
cost of slightly higher memory usage. A benchmark run looks like below.
GLIBC_TUNABLES=glibc.malloc.hugetlb=1 taskset -c 0 ./run.sh deals/1k
The solver fully analyzed 1000 random deals (under deals/1k) in just 80.0 seconds,
averaging more than 12 deals per second. Below are time and memory distributions,
with both maximums from deal.310.
| Percentile | 50th | 75th | 90th | 95th | 99th | 99.9th | Max |
|---|---|---|---|---|---|---|---|
| Time (s) | 0.05 | 0.10 | 0.17 | 0.24 | 0.38 | 0.54 | 0.77 |
| Memory (MiB) | 7.3 | 8.9 | 11.8 | 14.3 | 20.3 | 30.6 | 32.3 |
One of the most difficult deals is this symmetric one, with four void suits and nobody holding consecutive ranks in any suit. It took the solver less than 2.5 seconds.
♠ - ♥ Q853 ♦ AJ962 ♣ KT74
♠ KT74 ♥ - ♦ Q853 ♣ AJ962 ♠ Q853 ♥ AJ962 ♦ KT74 ♣ -
♠ AJ962 ♥ KT74 ♦ - ♣ Q853
N 5 5 5 5 1.02 s 82.5 M
S 4 4 8 7 1.30 s 82.5 M
H 8 7 4 4 1.65 s 82.5 M
D 4 4 7 8 1.97 s 82.5 M
C 7 8 4 4 2.26 s 82.5 M
An even more freakish deal with each player holding only two suits made the solver work hard for more than 10 seconds!
♠ KJ9753 ♥ - ♦ AQT8642 ♣ -
♠ AQT8642 ♥ KJ9753 ♦ - ♣ - ♠ - ♥ - ♦ KJ9753 ♣ AQT8642
♠ - ♥ AQT8642 ♦ - ♣ KJ9753
N 7 7 7 7 5.65 s 118.2 M
S 6 6 7 7 6.43 s 118.2 M
H 7 7 6 6 7.92 s 118.2 M
D 7 7 6 6 9.70 s 118.5 M
C 6 6 7 7 10.69 s 118.5 M
A new champion has emerged when North and South switch hands in the symmetric three-suited deal above. This simple change surprisingly increases the solving time by more than 20x and the memory usage by 20x, overwhelmingly just for NT contracts.
♠ AJ962 ♥ KT74 ♦ - ♣ Q853
♠ KT74 ♥ - ♦ Q853 ♣ AJ962 ♠ Q853 ♥ AJ962 ♦ KT74 ♣ -
♠ - ♥ Q853 ♦ AJ962 ♣ KT74
N 7 7 7 7 49.88 s 1649.8 M
S 4 4 7 7 50.19 s 1649.8 M
H 7 7 4 4 50.45 s 1649.8 M
D 4 4 7 7 50.68 s 1649.8 M
C 7 7 4 4 50.97 s 1649.8 M
The table below shows the time for solving 1000 random deals in deals/1k with multiple cores.
The solver is single-threaded, so multiple instances of the solver are running in parallel.
| # Cores | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| Time (s) | 80.0 | 46.6 | 24.6 | 15.4 | 12.6 |
| Speed-up | 1.0 | 1.7 | 3.3 | 5.2 | 6.3 |
The scaling is decent up to 8 cores. 16 cores give small additional speed-up as the cores are SMT threads rather than physical cores.
Jul 2023 For single-threaded performance, the solver is 1.36x faster than
DDS 2.9 and 1.75x faster than
Bridge Calculator (bcalc) on 5000 random deals.
The detailed run log is comparison/results.5k_deals.txt.
Since all the solvers are super fast on modern hardware, the difference is only noticeable after 80 percentile as shown in the plot below.
A log-scale plot magnifies the difference. The gap between this solver and DDS is slightly wider than the gap between DDS and bcalc.
Sep 2026 update: this solver has improved by 50% since the above comparison, so it's 2.0x faster than DDS 2.9 and 2.6x faster than bcalc now. Performance improvements seem to have stagnated with both DDS and bcalc.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.

