The DDS (Double Dummy Solver) library provides a Python interface for analyzing bridge hands using the double-dummy solver. This interface allows you to calculate trick distribution, par scores, and other double-dummy analysis from Python.
- Python 3.10+ (tested with 3.10, 3.11, 3.12, 3.14)
- Bazel 7.x
- C++ compiler (clang 15+ or GCC 11+)
# Build the Python extension and Python package wrapper
bazel build //python:dds3_lib
# Build wheel artifact
bazel build //python:dds3_wheel_dist
# Build with optimizations
bazel build -c opt //python:_dds3
# Build with debug symbols
bazel build -c dbg //python:_dds3The compiled extension will be located at bazel-bin/python/_dds3.so.
For wheel packaging, the extension is also copied into the package as dds3/_dds3.so.
# Create a virtual environment (optional but recommended)
python -m venv venv
source venv/bin/activate
# Install pytest (if not already installed)
pip install pytest# Set PYTHONPATH to include source package and top-level extension fallback
export PYTHONPATH=python:bazel-bin/python
# Run Bazel smoke test for Python bindings
bazel test //python:python_interface_smoke_test
# Or use pytest directly
pytest python/tests/ -v
# Run specific test file
pytest python/tests/test_solve_board.py -vThe Python interface includes 65 comprehensive unit tests covering:
- Type validation and boundary checking
- PBN (Portable Bridge Notation) parsing
- Array/sequence conversions
- Error handling and exception propagation
- Default parameter behavior
- Solver invocation, result structure, and API integration (not full numerical validation of DDS solver results)
Solves a single bridge deal using binary card format.
Parameters:
deal(dict): Dictionary with keys:trump(int, 0-4): Trump suit (0=♠, 1=♥, 2=♦, 3=♣, 4=NT)first(int, 0-3): Player to lead (0=North, 1=East, 2=South, 3=West)remain_cards(list[list[int]]): 4x4 array of bitmasks,[hand][suit]current_trick_suit(tuple[int, int, int]): Current trick suits (0-3)current_trick_rank(tuple[int, int, int]): Current trick ranks (0 or 2-14; 0 = unset)
Returns:
- dict with keys:
nodes,cards,suit,rank,equals,score
Example:
from dds3 import solve_board
deal = {
"trump": 0, # Spades
"first": 0, # North leads
"remain_cards": [
[0x7FFC, 0, 0, 0], # North: all spades
[0, 0x7FFC, 0, 0], # East: all hearts
[0, 0, 0x7FFC, 0], # South: all diamonds
[0, 0, 0, 0x7FFC], # West: all clubs
],
"current_trick_suit": (0, 0, 0),
"current_trick_rank": (0, 0, 0),
}
result = solve_board(deal)
print(f"Tricks available: {result['score']}")solve_board_pbn(remain_cards, trump=4, first=0, current_trick_suit=(0,0,0), current_trick_rank=(0,0,0), target=-1, solutions=3, mode=0, thread_index=0)
Solves a single bridge deal using PBN (Portable Bridge Notation).
Parameters:
remain_cards(str): PBN string (e.g., "N:AK.234.456.789TJQ W:QJ.AKQJ.789.234 E:T9.T9.TJ.AK S:8765.8765.AKQJ32.6")trump(int, default=4): Trump suit (0-4)first(int, default=0): Player to leadcurrent_trick_suit(tuple, default=(0,0,0)): Current trick suitscurrent_trick_rank(tuple, default=(0,0,0)): Current trick ranks (0 or 2-14; 0 = unset)- Other parameters: same as
solve_board
Returns:
- dict with keys:
nodes,cards,suit,rank,equals,score
Example:
from dds3 import solve_board_pbn
pbn = "N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3"
result = solve_board_pbn(pbn, trump=1) # Hearts
print(f"Tricks: {result['score']}")Calculates the double-dummy table for all contracts and strains.
Parameters:
table_deal(dict): Dictionary with key:cards(list[list[int]]): 4x4 array of bitmasks,[hand][suit]
Returns:
- dict with key:
res_tableres_table: 5x4 array whereres_table[strain][hand]= tricks available
Example:
from dds3 import calc_dd_table
table_deal = {
"cards": [
[0x7FFC, 0, 0, 0],
[0, 0x7FFC, 0, 0],
[0, 0, 0x7FFC, 0],
[0, 0, 0, 0x7FFC],
],
}
result = calc_dd_table(table_deal)
# result['res_table'][0][0] = tricks for spades, North
# result['res_table'][4][0] = tricks for NT, NorthCalculates double-dummy tables for multiple PBN deals with optional par scores.
Parameters:
deals_pbn(list[str]): List of PBN stringsmode(int, default=-1): Par vulnerability / calculation mode-1: Disable par calculation (par_results will be empty list)0: None vulnerable1: Both vulnerable2: North-South vulnerable3: East-West vulnerable
trump_filter(sequence[int], default=(0,0,0,0,0)): Strains to skip (0=include, 1=skip)- Accepts any sequence type (list, tuple, etc.)
- Order: [♠, ♥, ♦, ♣, NT]
Returns:
- dict with keys:
no_of_boards,tables,par_results(empty list when mode=-1)tables[i]['res_table']is always a 5x4 matrix in fixed strain order:[♠, ♥, ♦, ♣, NT]res_table[strain][hand]gives tricks for that strain/hand- If a strain is skipped by
trump_filter, that row is present but zero-filled
Example:
from dds3 import calc_all_tables_pbn
deals = [
"N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3",
"N:AK.234.456.789TJQ W:QJ.AKQJ.789.234 E:T9.T9.TJ.AK S:8765.8765.AKQJ32.6",
]
result = calc_all_tables_pbn(deals, mode=0)
print(f"Boards analyzed: {result['no_of_boards']}")
print(f"Par results: {result['par_results']}")Calculates par contracts and scores for a given double-dummy table.
Parameters:
table_results(dict): DD table result with key:res_table: 5x4 array fromcalc_dd_table
vulnerable(int, default=0): Vulnerability (0=none, 1=both, 2=NS, 3=EW)
Returns:
- dict with keys:
par_contracts_string,par_score
Example:
from dds3 import calc_dd_table, par
table_deal = {
"cards": [
[0x7FFC, 0, 0, 0],
[0, 0x7FFC, 0, 0],
[0, 0, 0x7FFC, 0],
[0, 0, 0, 0x7FFC],
],
}
dd_result = calc_dd_table(table_deal)
par_result = par(dd_result, vulnerable=0)
print(f"Par: {par_result['par_score']}")
print(f"Contract: {par_result['par_contracts_string']}")Cards are represented using DDS rank bitmasks shifted left by 2:
- 2 =
0x0004 - 3 =
0x0008 - ...
- A =
0x4000
Examples:
0x0004= 2 only0x0008= 3 only0x4000= A only0x7FFC= All cards (A-K-Q-J-T-9-8-7-6-5-4-3-2)
The remain_cards array format is [hand][suit]:
remain_cards = [
[north_spades, north_hearts, north_diamonds, north_clubs],
[east_spades, east_hearts, east_diamonds, east_clubs],
[south_spades, south_hearts, south_diamonds, south_clubs],
[west_spades, west_hearts, west_diamonds, west_clubs],
]Portable Bridge Notation format: "N:AK.234.456.789TJQ W:QJ.AKQJ.789.234 E:T9.T9.TJ.AK S:8765.8765.AKQJ32.6"
Format: [Seat]:[Spades].[Hearts].[Diamonds].[Clubs]
- Seats: N (North), E (East), S (South), W (West)
- Cards: 2-9, T (10), J, Q, K, A (highest)
- Dots separate suits
- Omitted cards belong to other players
The Python interface validates all inputs:
- Suit values: 0-3 for bids, 0-4 for trump
- Rank values: 0 or 2-14 for trick cards (
0means unset) - Card bitmasks: 0..0x7FFC
- Array dimensions: 4x4 for card arrays, 5x4 for results
- PBN format: Must be valid PBN notation
ValueError: Invalid input parameters (bounds, format)RuntimeError: DDS solver errors (e.g., invalid board state)KeyError: Missing required dictionary keys
Example:
from dds3 import solve_board
# This will raise ValueError for invalid suit
try:
deal = {
"trump": 5, # Invalid: must be 0-4
"first": 0,
"remain_cards": [[0, 0, 0, 0]] * 4,
"current_trick_suit": (0, 0, 0),
"current_trick_rank": (0, 0, 0),
}
solve_board(deal)
except ValueError as e:
print(f"Validation error: {e}")
# This will raise RuntimeError if DDS detects invalid board state
try:
deal = {
"trump": 0,
"first": 0,
"remain_cards": [[0, 0, 0, 0]] * 4, # Empty board
"current_trick_suit": (0, 0, 0),
"current_trick_rank": (0, 0, 0),
}
solve_board(deal)
except RuntimeError as e:
print(f"DDS error: {e}")- The extension is thread-safe for most operations
- Use
thread_indexparameter for multi-threaded solving (0-based index) - For batch processing, prefer
calc_all_tables_pbnover multiplesolve_board_pbncalls - Consider using optimized builds (
bazel build -c opt) for performance-critical code
# Install prerequisites
brew install bazelisk
# Build
bazelisk build -c opt //python:_dds3The Bazel build downloads the pinned LLVM toolchain automatically via
bazel-contrib/toolchains_llvm, so no separate Homebrew LLVM installation is
required.
# Install prerequisites (Ubuntu/Debian)
sudo apt-get install build-essential python3-dev
# Build
bazel build -c opt //python:_dds3On Linux hosts where a pinned LLVM toolchain is configured (for example,
linux-x86_64), Bazel also resolves that pinned LLVM toolchain automatically
during the build. On other Linux architectures, Bazel falls back to its
default C++ toolchain resolution (typically using the system compiler).
Currently not officially supported. Contributions welcome!
Ensure PYTHONPATH includes both source and built extension:
export PYTHONPATH=python:bazel-bin/pythonCheck that list/array types match expectations:
trump_filteraccepts any sequence (list, tuple, etc.)current_trick_suitandcurrent_trick_rankaccept both lists and tuplescardsandremain_cardsmust be lists of lists
Refer to DDS error codes in the C++ library documentation. Common ones:
- Error -2: Invalid board state (e.g., wrong card count)
- Error -14: Wrong number of remaining cards
For questions, bug reports, or feature requests, please open an issue on GitHub.
The Python interface follows the same license as the DDS library.