Skip to content
Merged
35 changes: 32 additions & 3 deletions docs/python_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,17 +238,46 @@ 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)`
#### `analyse_play(deal, play_suit, play_rank, context=None)`

Returns the double-dummy trick count after each card of a played hand (wraps `AnalysePlayPBN`).
Returns the double-dummy trick count after each card of a played hand, binary format. Wraps the
C++ `analyse_play()` entry point directly.

**Parameters:**
- `deal` (dict): Deal dict with keys `trump`, `first`, `remain_cards`, `current_trick_suit`,
`current_trick_rank` (see `solve_board`)
- `play_suit` (seq): Suit of each card played, in order (0=♠, 1=♥, 2=♦, 3=♣)
- `play_rank` (seq): Rank of each card played, in order (2-14), same length as `play_suit`
- `context` (SolverContext, optional): Reusable solver context for efficiency

**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

deal = {
"trump": 4, "first": 0,
"remain_cards": [...],
"current_trick_suit": (0, 0, 0), "current_trick_rank": (0, 0, 0),
}
result = analyse_play(deal, play_suit=[0], play_rank=[6])
print(result["tricks"])
```

#### `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, context=None)`

Returns the double-dummy trick count after each card of a played hand. Converts PBN to binary and
calls the `analyse_play()` entry point directly.

**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
- `thread_index` (int, default=0): Ignored; use `context` instead
- `context` (SolverContext, optional): Reusable solver context for efficiency

**Returns:**
- dict with keys: `number` (int), `tricks` (list[int]) — `tricks[i]` is the trick count for the side to play after `i` cards.
Expand Down
2 changes: 2 additions & 0 deletions jni/exported_symbols.lds
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ _SolveAllChunksBin
_SolveAllChunksPBN
_SolveBoard
_SolveBoardPBN
_dds_c_analyse_play
_dds_c_calc_dd_table
_dds_c_calc_dd_table_pbn
_dds_c_calc_par
_dds_c_clear_tt
_dds_c_configure_tt
_dds_c_convert_from_pbn
_dds_c_convert_play_from_pbn
_dds_c_convert_to_dealer_text_format
_dds_c_convert_to_sides_text_format
_dds_c_create_solvercontext
Expand Down
2 changes: 2 additions & 0 deletions jni/version_script.lds
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
SolveAllChunksPBN;
SolveBoard;
SolveBoardPBN;
dds_c_analyse_play;
dds_c_calc_dd_table;
dds_c_calc_dd_table_pbn;
dds_c_calc_par;
dds_c_clear_tt;
dds_c_configure_tt;
dds_c_convert_from_pbn;
dds_c_convert_play_from_pbn;
dds_c_convert_to_dealer_text_format;
dds_c_convert_to_sides_text_format;
dds_c_create_solvercontext;
Expand Down
163 changes: 163 additions & 0 deletions library/src/analyse_play_adapter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include <api/analyse_play.hpp>
#include <solver_if.hpp>

/**
* @brief Analyze a sequence of played cards (binary format) and determine the tricks taken.
*
* This function simulates play of a bridge Deal according to the provided play trace,
* using double dummy analysis to determine the number of tricks won at each step.
*
* @param ctx Solver context containing state and resources, reused across the
* initial solve and every subsequent per-card analyse_later_board
* call so the hint-bounded incremental searches see a warm TT.
* @param dl_in The Deal to analyze
* @param play The sequence of played cards (binary format)
* @param solvedp Pointer to result structure for solved play
* @return RETURN_NO_FAULT on success, error code otherwise
*/
auto analyse_play(
SolverContext& ctx,
const Deal& dl_in,
const PlayTraceBin& play,
SolvedPlay* solvedp) -> int
{
Deal dl = dl_in;

MoveType move;
FutureTricks fut;

int ret = solve_board_internal(ctx, dl, -1, 1, 1, &fut);
if (ret != RETURN_NO_FAULT)
return ret;
const int ini_depth = ctx.search().ini_depth();
const int numTricks = ((ini_depth + 3) >> 2) + 1;
const int numCardsPlayed = ((48 - ini_depth) % 4) + 1;

int last_trick = (play.number + 3) / 4;
int last_card = ((play.number + 3) % 4) + 1;
if (last_trick >= numTricks)
{
last_trick = numTricks-1;
last_card = 4;
}
solvedp->number = 0;

solvedp->tricks[0] = (numCardsPlayed % 2 == 1 ?
numTricks - fut.score[0] : fut.score[0]);
int hint = solvedp->tricks[0];
int hintDir;

int running_remainder = numTricks;
int running_declarer = 0;
int running_player = dl.first;
int running_side = 1; /* defenders */
int start_side = running_player % 2;

for (int trick = 1; trick <= last_trick; trick++)
{
int best_card = 0, best_suit = 0, best_player = 0, trump_played = 0;
int lc = (trick == last_trick ? last_card : 4);

bool haveCurrent = (numCardsPlayed > 1 && trick == 1);
int offset = 4 * (trick - 1) - (numCardsPlayed - 1);

for (int card = 1; card <= lc; card++)
{
int suit, rr;
bool usingCurrent = (haveCurrent && card < numCardsPlayed);
if (usingCurrent)
{
suit = dl.currentTrickSuit[card - 1];
rr = dl.currentTrickRank[card - 1];
}
else
{
suit = play.suit[offset + card - 1];
rr = play.rank[offset + card - 1];
}
unsigned hold = static_cast<unsigned>(bit_map_rank[rr] << 2);

move.suit = suit;
move.rank = rr;
move.sequence = rr;

/* Keep track of the winner of the trick so far */
if (card == 1)
{
best_card = rr;
best_suit = suit;
best_player = dl.first;
trump_played = (suit == dl.trump ? 1 : 0);
}
else if (suit == dl.trump)
{
if (! trump_played || rr > best_card)
{
best_card = rr;
best_suit = suit;
best_player = running_player;
trump_played = 1;
}
}
else if (! trump_played && suit == best_suit && rr > best_card)
{
best_card = rr;
best_player = running_player;
}

if ((dl.remainCards[running_player][suit] & hold) == 0)
{
if (! usingCurrent)
return RETURN_PLAY_FAULT;
}
else
dl.remainCards[running_player][suit] ^= hold;

if (card == 4)
{
running_declarer += (best_player % 2 == start_side ? 0 : 1);
running_remainder--;

if ((dl.first + best_player) % 2 == 0)
{
hintDir = 0; // Same side leads again; lower bound
hint = running_remainder - fut.score[0];
}
else
{
hintDir = 1; // Other ("our") side wins trick; upper bound
hint = fut.score[0] - 1;
}

dl.first = best_player;
running_side = (dl.first % 2 == start_side ? 1 : 0);
running_player = dl.first;
}
else
{
running_player = (running_player + 1) % 4;
running_side = 1 - running_side;
hint = running_remainder - fut.score[0];
hintDir = 0;
}

if (usingCurrent)
continue;

if ((ret = analyse_later_board(ctx, dl.first, &move, hint,
hintDir, &fut))
!= RETURN_NO_FAULT)
{
return ret;
}

int new_solved_decl = running_declarer + (running_side ?
running_remainder - fut.score[0] : fut.score[0]);

solvedp->tricks[offset + card] = new_solved_decl;
}
}
solvedp->number = 4 * last_trick + last_card - 3 - (numCardsPlayed - 1);

return RETURN_NO_FAULT;
}
1 change: 1 addition & 0 deletions library/src/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ cc_library(
cc_library(
name = "api_definitions",
hdrs = [
"analyse_play.hpp",
"calc_dd_table.hpp",
"calc_par.hpp",
"dds.h",
Expand Down
40 changes: 40 additions & 0 deletions library/src/api/analyse_play.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @file analyse_play.hpp
* @brief C++ interface for play analysis with explicit solver context.
*
* This header provides the C++-only, context-aware entry point for play
* analysis, allowing clients to manage solver state across multiple
* analyse_play calls.
*
* Naming note: this mirrors the legacy AnalysePlayBin/AnalysePlayPBN API
* family, so "analyse" (British spelling) is kept deliberately here,
* matching that existing name rather than switching to "analyze".
*
* @copyright (C) 2006-2014 by Bo Haglund / 2014-2018 by Bo Haglund & Soren Hein.
* @see LICENSE and README.
*/

#pragma once

#include <api/dds_data_types.hpp>
#include <solver_context/solver_context.hpp>

/**
* @brief Analyze a sequence of played cards (binary format) with explicit solver context.
*
* C++-only overload that accepts an explicit SolverContext, allowing clients
* to manage solver state and resources across multiple analyse_play calls.
* This is the preferred C++ API following DDS 3 snake_case naming conventions.
*
* @param ctx Solver context containing state and resources
* @param dl Deal to analyze
* @param play Sequence of played cards (binary format)
* @param solvedp Output structure for the solved play
* @return Error code (RETURN_NO_FAULT on success)
* @see AnalysePlayBin() for the backward-compatible PascalCase legacy version
*/
auto analyse_play(
SolverContext& ctx,
const Deal& dl,
const PlayTraceBin& play,
SolvedPlay* solvedp) -> int;
7 changes: 7 additions & 0 deletions library/src/api/dds_api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <solver_context/solver_context.hpp>
#include <api/solve_board.hpp>
#include <api/calc_dd_table.hpp>
#include <api/analyse_play.hpp>

extern "C" {

Expand Down Expand Up @@ -64,5 +65,11 @@ extern "C" {
DdTableResults* table_results,
ParResults* par_results) -> int;

EXTERN_C DLLEXPORT auto dds_analyse_play(
DDS_SOLVER_CTX ctx,
const Deal& dl,
const PlayTraceBin& play,
SolvedPlay* solvedp) -> int;


}
32 changes: 32 additions & 0 deletions library/src/api/dds_c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,38 @@ DLLEXPORT int dds_c_convert_from_pbn(const char* pbn_deal,
}
}

DLLEXPORT int dds_c_analyse_play(
DDS_C_SOLVER_CTX ctx,
const struct Deal* dl,
const struct PlayTraceBin* play,
struct SolvedPlay* solved)
{
if (ctx == nullptr || dl == nullptr || play == nullptr || solved == nullptr)
return RETURN_UNKNOWN_FAULT;
Comment thread
zzcgumn marked this conversation as resolved.
Resolved

try {
return dds_analyse_play(static_cast<SolverContext*>(ctx), *dl, *play, solved);
} catch (...) {
return RETURN_UNKNOWN_FAULT;
}
}

DLLEXPORT int dds_c_convert_play_from_pbn(
const struct PlayTracePBN* pbn_play,
struct PlayTraceBin* bin_play)
{
if (pbn_play == nullptr || bin_play == nullptr)
return RETURN_UNKNOWN_FAULT;

try {
/* convert_play_from_pbn already returns a proper RETURN_* code
(unlike convert_from_pbn's bare 1/0), so forward it as-is. */
return convert_play_from_pbn(*pbn_play, *bin_play);
} catch (...) {
return RETURN_UNKNOWN_FAULT;
}
}

DLLEXPORT void dds_c_configure_tt(DDS_C_SOLVER_CTX ctx, int tt_kind,
int def_mb, int max_mb)
{
Expand Down
24 changes: 24 additions & 0 deletions library/src/api/dds_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,30 @@ DLLEXPORT int dds_c_calc_dd_table_pbn(DDS_C_SOLVER_CTX ctx,
DLLEXPORT int dds_c_convert_from_pbn(const char* pbn_deal,
unsigned int cards[DDS_HANDS][DDS_SUITS]);

/* Analyse a sequence of played cards (binary format) and determine the
double-dummy trick count for the side to play after each card. Reuses
ctx's transposition table across the initial solve and every subsequent
per-card search, so repeated calls on the same context benefit from a
warm TT. There is deliberately no *_pbn twin on this shim -- same
rationale as dds_c_solve_board / dds_c_calc_par: a PBN-based caller
converts once with dds_c_convert_from_pbn (deal) and
dds_c_convert_play_from_pbn (play), then calls this entry point
directly. */
DLLEXPORT int dds_c_analyse_play(
DDS_C_SOLVER_CTX ctx,
const struct Deal* dl,
const struct PlayTraceBin* play,
struct SolvedPlay* solved);

/* Parse a PBN-format play trace (two characters per card: suit letter
S/H/D/C followed by rank 2-9/T/J/Q/K/A) into the binary PlayTraceBin
consumed by dds_c_analyse_play. Needs no SolverContext. Returns
RETURN_PLAY_FAULT if pbn_play->number is out of [0, 52] or any card is
malformed, else RETURN_NO_FAULT. */
DLLEXPORT int dds_c_convert_play_from_pbn(
const struct PlayTracePBN* pbn_play,
struct PlayTraceBin* bin_play);

/* Transposition-table configuration. */
DLLEXPORT void dds_c_configure_tt(DDS_C_SOLVER_CTX ctx, int tt_kind,
int def_mb, int max_mb);
Expand Down
1 change: 1 addition & 0 deletions library/src/dds.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <dds/dds.h>
#endif

#include <api/analyse_play.hpp>
#include <api/calc_dd_table.hpp>
#include <api/calc_par.hpp>
#include <api/solve_board.hpp>
Expand Down
5 changes: 5 additions & 0 deletions library/src/dds_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,8 @@ DLLEXPORT auto dds_calc_par(
return calc_par(*ctx, table_deal, vulnerable, table_results, par_results);
}

DLLEXPORT auto dds_analyse_play(DDS_SOLVER_CTX ctx, const Deal& dl, const PlayTraceBin& play, SolvedPlay* solvedp) -> int
{
return analyse_play(*ctx, dl, play, solvedp);
}

Loading
Loading