Skip to content

Latest commit

 

History

History
375 lines (291 loc) · 10.9 KB

File metadata and controls

375 lines (291 loc) · 10.9 KB

Python Interface Documentation

Overview

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.

Building the Python Interface

Prerequisites

  • Python 3.10+ (tested with 3.10, 3.11, 3.12, 3.14)
  • Bazel 7.x
  • C++ compiler (clang 15+ or GCC 11+)

Build Instructions

# 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:_dds3

The 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.

Installation and Testing

Setup

# Create a virtual environment (optional but recommended)
python -m venv venv
source venv/bin/activate

# Install pytest (if not already installed)
pip install pytest

Running Unit Tests

# 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 -v

Test Coverage

The 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)

API Reference

Core Functions

solve_board(deal, target=-1, solutions=3, mode=0, thread_index=0)

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 lead
  • current_trick_suit (tuple, default=(0,0,0)): Current trick suits
  • current_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']}")

calc_dd_table(table_deal)

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_table
    • res_table: 5x4 array where res_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, North

calc_all_tables_pbn(deals_pbn, mode=-1, trump_filter=[0,0,0,0,0])

Calculates double-dummy tables for multiple PBN deals with optional par scores.

Parameters:

  • deals_pbn (list[str]): List of PBN strings
  • mode (int, default=-1): Par vulnerability / calculation mode
    • -1: Disable par calculation (par_results will be empty list)
    • 0: None vulnerable
    • 1: Both vulnerable
    • 2: North-South vulnerable
    • 3: 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']}")

par(table_results, vulnerable=0)

Calculates par contracts and scores for a given double-dummy table.

Parameters:

  • table_results (dict): DD table result with key:
    • res_table: 5x4 array from calc_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']}")

Card Representation

Binary Format (remain_cards)

Cards are represented using DDS rank bitmasks shifted left by 2:

  • 2 = 0x0004
  • 3 = 0x0008
  • ...
  • A = 0x4000

Examples:

  • 0x0004 = 2 only
  • 0x0008 = 3 only
  • 0x4000 = A only
  • 0x7FFC = 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],
]

PBN Format

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

Validation and Error Handling

Input Validation

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 (0 means unset)
  • Card bitmasks: 0..0x7FFC
  • Array dimensions: 4x4 for card arrays, 5x4 for results
  • PBN format: Must be valid PBN notation

Exception Handling

  • 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}")

Performance Considerations

  • The extension is thread-safe for most operations
  • Use thread_index parameter for multi-threaded solving (0-based index)
  • For batch processing, prefer calc_all_tables_pbn over multiple solve_board_pbn calls
  • Consider using optimized builds (bazel build -c opt) for performance-critical code

Building from Source

macOS

# Install prerequisites
brew install bazelisk

# Build
bazelisk build -c opt //python:_dds3

The Bazel build downloads the pinned LLVM toolchain automatically via bazel-contrib/toolchains_llvm, so no separate Homebrew LLVM installation is required.

Linux

# Install prerequisites (Ubuntu/Debian)
sudo apt-get install build-essential python3-dev

# Build
bazel build -c opt //python:_dds3

On 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).

Windows

Currently not officially supported. Contributions welcome!

Troubleshooting

Import Error: ModuleNotFoundError: No module named 'dds3'

Ensure PYTHONPATH includes both source and built extension:

export PYTHONPATH=python:bazel-bin/python

"incompatible function arguments"

Check that list/array types match expectations:

  • trump_filter accepts any sequence (list, tuple, etc.)
  • current_trick_suit and current_trick_rank accept both lists and tuples
  • cards and remain_cards must be lists of lists

DDS errors

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

Contributing

For questions, bug reports, or feature requests, please open an issue on GitHub.

License

The Python interface follows the same license as the DDS library.