dds-bridge-sys 3.3.0

Generated bindings to DDS, the double dummy solver for bridge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# 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

```bash
# 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
```bash
# 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
```bash
# 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:**
```python
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:**
```python
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:**
```python
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:**
```python
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:**
```python
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']}")
```

#### `dealer_par(table_results, dealer, vulnerable=0)`

Calculates par contracts and score from the dealer's perspective (wraps `DealerPar`).

**Parameters:**
- `table_results` (dict): DD table result with key `res_table` (e.g. from `calc_dd_table`)
- `dealer` (int): Dealer seat (0=N, 1=E, 2=S, 3=W)
- `vulnerable` (int, default=0): Vulnerability (0=none, 1=both, 2=NS, 3=EW)

**Returns:**
- dict with keys: `score` (int), `number` (int), `contracts` (list[str], length `number`)

```python
from dds3 import calc_dd_table, dealer_par

dd_result = calc_dd_table(table_deal)
result = dealer_par(dd_result, dealer=0, vulnerable=0)
print(result["score"], result["contracts"])
```

#### `analyse_play_pbn(remain_cards, play, trump=4, first=0, current_trick_suit=(0,0,0), current_trick_rank=(0,0,0), thread_index=0)`

Returns the double-dummy trick count after each card of a played hand (wraps `AnalysePlayPBN`).

**Parameters:**
- `remain_cards` (str): Full deal in PBN format before any card of `play`
- `play` (str): Cards played, 2 characters each (suit+rank), e.g. `"SAHK..."`
- `trump` (int, default=4): Trump suit (0=♠, 1=♥, 2=♦, 3=♣, 4=NT)
- `first` (int, default=0): Seat that leads (0=N, 1=E, 2=S, 3=W)
- `current_trick_suit` / `current_trick_rank` (seq, default=(0,0,0)): Cards already in the trick
- `thread_index` (int, default=0): Thread id

**Returns:**
- dict with keys: `number` (int), `tricks` (list[int]) — `tricks[i]` is the trick count for the side to play after `i` cards.

```python
from dds3 import analyse_play_pbn

deal = "N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3"
result = analyse_play_pbn(deal, play="S6", trump=4, first=0)
print(result["tricks"])
```

#### `analyse_all_plays_pbn(deals)`

Batched form of `analyse_play_pbn` (wraps `AnalyseAllPlaysPBN`).

**Parameters:**
- `deals` (list[dict]): Up to 200 dicts, each with `remain_cards` (str, required), `play`
  (str, required), and optional `trump`, `first`, `current_trick_suit`, `current_trick_rank`.

**Returns:**
- list[dict]: one `{number, tricks}` dict per deal (same shape as `analyse_play_pbn`).

#### `set_max_threads(user_threads=0)`

Legacy thread-resource hook (wraps the **deprecated** `SetMaxThreads` C API).

This does **not** control DDS's batch parallelism and is kept only for backward
compatibility. `solve_all_boards_*` already parallelise across the machine's
hardware threads automatically (via `solve_boards_n`) — the value passed here does
not size that pool. `analyse_all_plays_pbn` currently runs sequentially.
`user_threads` must be `>= 0` (`0` = auto); raises `ValueError` for negative values.

For per-board concurrency from Python, create one `SolverContext` per worker thread
and pass it to `solve_board` / `solve_board_pbn` (which release the GIL during the
search).

## 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]`:
```python
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:**
```python
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
```bash
# 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
```bash
# 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:
```bash
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.