#include <algorithm>
#include <array>
#include <stdexcept>
#include <string>
#include <pybind11/pybind11.h>
#include <api/calc_par.hpp>
#include <dds/dds.hpp>
#include <pbn.hpp>
#include <solver_context/solver_context.hpp>
#include "converters.hpp"
namespace py = pybind11;
namespace
{
auto throw_on_dds_error(const int code) -> void
{
if (code == RETURN_NO_FAULT) {
return;
}
std::array<char, 80> message{};
ErrorMessage(code, message.data());
const std::string error_text =
"DDS error " + std::to_string(code) + ": " + std::string(message.data());
switch (code) {
case RETURN_TRUMP_WRONG:
case RETURN_FIRST_WRONG:
case RETURN_PBN_FAULT:
case RETURN_TARGET_WRONG_LO:
case RETURN_TARGET_WRONG_HI:
case RETURN_SOLNS_WRONG_LO:
case RETURN_SOLNS_WRONG_HI:
case RETURN_THREAD_INDEX:
case RETURN_MODE_WRONG_LO:
case RETURN_MODE_WRONG_HI:
case RETURN_NO_SUIT:
case RETURN_TOO_MANY_TABLES:
throw py::value_error(error_text);
default:
throw std::runtime_error(error_text);
}
}
auto register_solve_bindings(py::module_& module) -> void
{
module.def(
"solve_board",
[](const py::dict& deal,
const int target,
const int solutions,
const int mode,
const int thread_index,
py::object context_obj) {
FutureTricks future_tricks{};
const Deal native_deal = dds3_python::dict_to_deal(deal);
SolverContext* context_ptr = nullptr;
if (!context_obj.is_none()) {
context_ptr = py::cast<SolverContext*>(context_obj);
}
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
if (context_ptr == nullptr) {
code = SolveBoard(
native_deal,
target,
solutions,
mode,
&future_tricks,
thread_index);
} else {
code = solve_board(
*context_ptr,
native_deal,
target,
solutions,
mode,
&future_tricks);
}
}
throw_on_dds_error(code);
return dds3_python::future_tricks_to_dict(future_tricks);
},
py::arg("deal"),
py::arg("target") = -1,
py::arg("solutions") = 3,
py::arg("mode") = 0,
py::arg("thread_index") = 0,
py::arg("context") = py::none(),
"Solve a single bridge deal from binary format.\n\n"
"Args:\n"
" deal (dict): Deal dict with keys 'trump', 'first', 'remain_cards', 'current_trick_suit', "
"'current_trick_rank'.\n"
" target (int, optional): Target number of tricks for optimization (-1 = no target). Default: -1\n"
" solutions (int, optional): Depth of search (1-3, higher = more branches). Default: 3\n"
" mode (int, optional): 0 = auto, 1 = thread depth 6, 2 = node depth 12. Default: 0\n"
" thread_index (int, optional): Thread ID for transposition table access. Default: 0\n"
" context (SolverContext, optional): Reusable solver context for efficiency. Default: None\n\n"
"Returns:\n"
" dict: Result dict with keys 'nodes', 'cards', 'suit', 'rank', 'equals', 'score'.\n\n"
"Raises:\n"
" ValueError: If input validation fails (invalid suit/rank range).\n"
" RuntimeError: If DDS solver returns error code.\n\n"
"Example (with context reuse for multiple boards):\n"
" context = dds3.SolverContext()\n"
" result1 = dds3.solve_board(deal1, context=context)\n"
" result2 = dds3.solve_board(deal2, context=context) # Reuses context");
module.def(
"solve_board_pbn",
[](const std::string& remain_cards,
const int trump,
const int first,
const py::sequence& current_trick_suit,
const py::sequence& current_trick_rank,
const int target,
const int solutions,
const int mode,
const int thread_index,
py::object context_obj) {
FutureTricks future_tricks{};
const DealPBN native_deal = dds3_python::pbn_to_deal(
remain_cards,
trump,
first,
current_trick_suit,
current_trick_rank);
SolverContext* context_ptr = nullptr;
if (!context_obj.is_none()) {
context_ptr = py::cast<SolverContext*>(context_obj);
}
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
if (context_ptr == nullptr) {
code = SolveBoardPBN(
native_deal,
target,
solutions,
mode,
&future_tricks,
thread_index);
} else {
Deal native_binary_deal{};
if (convert_from_pbn(native_deal.remainCards, native_binary_deal.remainCards) != RETURN_NO_FAULT) {
code = RETURN_PBN_FAULT;
} else {
for (int k = 0; k <= 2; ++k) {
native_binary_deal.currentTrickRank[k] = native_deal.currentTrickRank[k];
native_binary_deal.currentTrickSuit[k] = native_deal.currentTrickSuit[k];
}
native_binary_deal.first = native_deal.first;
native_binary_deal.trump = native_deal.trump;
code = solve_board(
*context_ptr,
native_binary_deal,
target,
solutions,
mode,
&future_tricks);
}
}
}
throw_on_dds_error(code);
return dds3_python::future_tricks_to_dict(future_tricks);
},
py::arg("remain_cards"),
py::arg("trump") = 4, py::arg("first") = 0, py::arg("current_trick_suit") = py::make_tuple(0, 0, 0),
py::arg("current_trick_rank") = py::make_tuple(0, 0, 0),
py::arg("target") = -1,
py::arg("solutions") = 3,
py::arg("mode") = 0,
py::arg("thread_index") = 0,
py::arg("context") = py::none(),
"Solve a single bridge deal from PBN (Portable Bridge Notation) format.\n\n"
"Args:\n"
" remain_cards (str): Remaining cards in PBN format (e.g., 'N:AK.234.456.789T...').\n"
" trump (int, optional): Trump suit (0=♠, 1=♥, 2=♦, 3=♣, 4=NT). Default: 4\n"
" first (int, optional): Seat that plays first (0=N, 1=E, 2=S, 3=W). Default: 0\n"
" current_trick_suit (tuple, optional): Suits in current trick (3-tuple of ints, 0-3). Default: (0, 0, 0)\n"
" current_trick_rank (tuple, optional): Ranks in current trick (3-tuple of ints, 0 or 2-14). Default: (0, 0, 0)\n"
" target (int, optional): Target number of tricks for optimization (-1 = no target). Default: -1\n"
" solutions (int, optional): Depth of search (1-3, higher = more branches). Default: 3\n"
" mode (int, optional): 0 = auto, 1 = thread depth 6, 2 = node depth 12. Default: 0\n"
" thread_index (int, optional): Thread ID for transposition table access. Default: 0\n"
" context (SolverContext, optional): Reusable solver context for efficiency. Default: None\n\n"
"Returns:\n"
" dict: Result dict with keys 'nodes', 'cards', 'suit', 'rank', 'equals', 'score'.\n\n"
"Raises:\n"
" ValueError: If PBN format is invalid or input validation fails.\n"
" RuntimeError: If DDS solver returns error code.");
}
auto register_table_bindings(py::module_& module) -> void
{
module.def(
"calc_dd_table",
[](const py::dict& table_deal) {
DdTableResults table_results{};
const DdTableDeal native_deal = dds3_python::dict_to_dd_table_deal(table_deal);
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
code = CalcDDtable(native_deal, &table_results);
}
throw_on_dds_error(code);
return dds3_python::dd_table_results_to_dict(table_results);
},
py::arg("table_deal"),
"Calculate the double-dummy table for all contracts and strains.\n\n"
"Args:\n"
" table_deal (dict): DD table deal dict with key 'cards' (4x4 nested list).\n\n"
"Returns:\n"
" dict: Double-dummy table with key 'res_table' (5x4 nested list).\n"
" res_table[strain][hand] = tricks available for that strain/hand.\n\n"
"Raises:\n"
" ValueError: If input validation fails (invalid card distribution).\n"
" RuntimeError: If DDS solver returns error code.");
module.def(
"calc_all_tables_pbn",
[](const py::list& deals_pbn, const int mode, const py::sequence& trump_filter) {
if (mode < -1 || mode > 3) {
throw py::value_error(
"mode has invalid value " + std::to_string(mode) +
" (expected -1=disabled, 0=none, 1=both, 2=NS, 3=EW)");
}
const auto trump_filter_vec = dds3_python::sequence_to_bounded_int_vector(
trump_filter,
DDS_STRAINS,
0,
1,
"trump_filter");
const int included_strains = static_cast<int>(std::count(
trump_filter_vec.begin(),
trump_filter_vec.end(),
0));
const bool wants_par = mode != -1;
const bool can_compute_par = included_strains == DDS_STRAINS;
if (wants_par && !can_compute_par) {
throw py::value_error(
"Par computation (mode != -1) requires all strains to be included "
"(trump_filter must be all zeros)");
}
const int max_tables =
(wants_par && can_compute_par)
? MAXNOOFTABLES
: ((included_strains > 0) ? ((MAXNOOFTABLES * DDS_STRAINS) / included_strains)
: MAXNOOFTABLES);
const auto native_deals = dds3_python::list_to_dd_table_deals_pbn(
deals_pbn,
static_cast<std::size_t>(max_tables));
DdTablesRes tables_res{};
AllParResults all_par_results{};
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
code = CalcAllTablesPBN(
&native_deals,
mode,
trump_filter_vec.data(),
&tables_res,
&all_par_results);
}
throw_on_dds_error(code);
py::dict result;
result["no_of_boards"] = tables_res.no_of_boards;
result["tables"] = dds3_python::dd_tables_res_to_list(tables_res, native_deals.no_of_tables);
if (wants_par && can_compute_par) {
result["par_results"] = dds3_python::all_par_results_to_list(
all_par_results,
native_deals.no_of_tables);
} else {
result["par_results"] = py::list(); }
return result;
},
py::arg("deals_pbn"),
py::arg("mode") = -1,
py::arg("trump_filter") = py::make_tuple(0, 0, 0, 0, 0),
"Calculate double-dummy tables for multiple PBN deals with optional par scores.\n\n"
"Args:\n"
" deals_pbn (list): List of PBN strings (e.g., ['N:AK.234.456.789T...', ...]).\n"
" mode (int, optional): Par vulnerability mode (-1=disabled, 0=none, 1=both, 2=NS, 3=EW). Default: -1\n"
" trump_filter (sequence, optional): Strains to skip (0=include, 1=skip). Default: (0,0,0,0,0)\n"
" Order: [♠, ♥, ♦, ♣, NT]\n\n"
"Returns:\n"
" dict: Result dict with keys:\n"
" 'no_of_boards' (int): Total number of calculated boards.\n"
" 'tables' (list): List of DD table dicts, one per input deal.\n"
" Each table dict contains 'res_table' (5x4) in fixed strain order\n"
" [♠, ♥, ♦, ♣, NT]. Rows for filtered-out strains are returned as zeros.\n"
" 'par_results' (list): List of par result dicts (empty when mode=-1).\n\n"
"Raises:\n"
" ValueError: If PBN format is invalid, trump_filter invalid, or too many tables.\n"
" RuntimeError: If DDS solver returns error code.");
}
auto register_par_bindings(py::module_& module) -> void
{
module.def(
"par",
[](const py::dict& table_results, const int vulnerable) {
if (vulnerable < 0 || vulnerable > 3) {
throw py::value_error(
"vulnerable has invalid value " + std::to_string(vulnerable) +
" (expected 0=none, 1=both, 2=NS, 3=EW)");
}
const DdTableResults native_table = dds3_python::dict_to_dd_table_results(table_results);
ParResults par_results{};
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
code = Par(&native_table, &par_results, vulnerable);
}
throw_on_dds_error(code);
return dds3_python::par_results_to_dict(par_results);
},
py::arg("table_results"),
py::arg("vulnerable") = 0,
"Calculate par contracts and scores for a given double-dummy table.\n\n"
"Args:\n"
" table_results (dict): DD table results dict with key 'res_table' (5x4 nested list).\n"
" vulnerable (int): Vulnerability (0=none, 1=both, 2=NS, 3=EW).\n\n"
"Returns:\n"
" dict: Par results with keys 'par_score' and 'par_contracts_string'.\n"
" par_contracts_string[ns] = contract string (e.g., '6NT+1', '7C=').\n\n"
"Raises:\n"
" ValueError: If input validation fails (invalid table or vulnerability).\n"
" RuntimeError: If DDS solver returns error code.");
}
auto register_calc_par_bindings(py::module_& module) -> void
{
module.def(
"calc_par",
[](const py::dict& table_deal, const int vulnerable) {
if (vulnerable < 0 || vulnerable > 3) {
throw py::value_error(
"vulnerable has invalid value " + std::to_string(vulnerable) +
" (expected 0=none, 1=both, 2=NS, 3=EW)");
}
const DdTableDeal native_deal = dds3_python::dict_to_dd_table_deal(table_deal);
DdTableResults table_results{};
ParResults par_results{};
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
code = calc_par(
native_deal,
vulnerable,
&table_results,
&par_results);
}
throw_on_dds_error(code);
py::dict result;
result["dd_table"] = dds3_python::dd_table_results_to_dict(table_results);
result["par_results"] = dds3_python::par_results_to_dict(par_results);
return result;
},
py::arg("table_deal"),
py::arg("vulnerable") = 0,
"Calculate double-dummy table and par contracts for a deal.\n\n"
"Combines CalcDDtable and Par operations in a single call. Creates a temporary\n"
"solver context internally for each call. For repeated calculations, prefer\n"
"calc_par_from_table if DD tables are already available, to avoid redundant\n"
"table computation.\n\n"
"Args:\n"
" table_deal (dict): Deal dict with key 'cards' (4x4 nested list of card bitmasks).\n"
" cards[hand][suit] where hand=0-3 (N,E,S,W), suit=0-3 (♠,♥,♦,♣)\n"
" Each card bitmask has bits 2-14 set for present ranks (2-A).\n"
" vulnerable (int): Vulnerability (0=none, 1=both, 2=NS, 3=EW). Default: 0\n\n"
"Returns:\n"
" dict: Result dict with two keys:\n"
" 'dd_table': DD table results (key 'res_table' = 5x4 nested list)\n"
" 'par_results': Par results (keys 'par_score' and 'par_contracts_string')\n\n"
"Raises:\n"
" ValueError: If input validation fails (invalid cards or vulnerability).\n"
" RuntimeError: If DDS solver returns error code.");
module.def(
"calc_par_from_table",
[](const py::dict& table_results, const int vulnerable) {
if (vulnerable < 0 || vulnerable > 3) {
throw py::value_error(
"vulnerable has invalid value " + std::to_string(vulnerable) +
" (expected 0=none, 1=both, 2=NS, 3=EW)");
}
const DdTableResults native_table = dds3_python::dict_to_dd_table_results(table_results);
ParResults par_results{};
int code = RETURN_NO_FAULT;
{
py::gil_scoped_release release;
code = calc_par_from_table(
&native_table,
vulnerable,
&par_results);
}
throw_on_dds_error(code);
return dds3_python::par_results_to_dict(par_results);
},
py::arg("table_results"),
py::arg("vulnerable") = 0,
"Calculate par contracts from a pre-computed double-dummy table.\n\n"
"Lightweight alternative to calc_par when the DD table is already available.\n"
"More efficient than calc_par when computing par for multiple deals with the same DD table,\n"
"or when par needs to be recalculated with different vulnerability.\n\n"
"Args:\n"
" table_results (dict): DD table results dict with key 'res_table' (5x4 nested list).\n"
" vulnerable (int): Vulnerability (0=none, 1=both, 2=NS, 3=EW). Default: 0\n\n"
"Returns:\n"
" dict: Par results with keys 'par_score' and 'par_contracts_string'.\n"
" par_contracts_string[ns] = contract string (e.g., '6NT+1', '7C=').\n\n"
"Raises:\n"
" ValueError: If input validation fails (invalid table or vulnerability).\n"
" RuntimeError: If DDS solver returns error code.");
}
}
PYBIND11_MODULE(_dds3, module)
{
module.doc() = "dds3 Python extension (MVP wrappers)";
py::class_<SolverContext>(
module,
"SolverContext",
"A reusable solver context that maintains state across multiple solve operations.\n\n"
"Creating a single context and reusing it for multiple solve_board calls is more\n"
"efficient than creating a new context for each call.\n\n"
"Example:\n"
" context = dds3.SolverContext()\n"
" result1 = dds3.solve_board(deal1, context=context)\n"
" result2 = dds3.solve_board(deal2, context=context) # Reuses cached state\n")
.def(py::init<>(), "Create a new solver context.");
register_solve_bindings(module);
register_table_bindings(module);
register_par_bindings(module);
register_calc_par_bindings(module);
module.def("api_root", []() {
return "dds.hpp";
});
module.def("module_name", []() {
return "_dds3";
});
}