Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,7 @@ doxygen_output/
# Claude Code local settings written by cce init
.claude/settings.local.json
/dotnet/DDS_Core/.github/copilot-instructions.md

# No need to clutter the repo with these.
# regenerate_hand_lists.sh will recreate them whenever needed.
hands/partial/
4 changes: 3 additions & 1 deletion library/src/calc_dd_table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ auto calc_dd_table(
return res;

// Populate result table from solved boards
const int tricks = remaining_tricks_from_holdings(table_deal.cards);
for (int index = 0; index < DDS_STRAINS; index++)
{
int strain = bo.deals[index].trump;

for (int first = 0; first < DDS_HANDS; first++)
{
table_results->res_table[strain][ rho[first] ] =
13 - solved.solved_board[index].score[first];
declarer_tricks_from_leader_score(
tricks, solved.solved_board[index].score[first]);
}
}
return RETURN_NO_FAULT;
Expand Down
92 changes: 82 additions & 10 deletions library/src/calc_tables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <numeric>
#include <vector>

#include <lookup_tables/lookup_tables.hpp>
#include <pbn.hpp>
#include <solve_board.hpp>
#include <api/solve_board.hpp>
Expand All @@ -33,6 +34,43 @@ auto calc_all_boards_n(
int max_threads = 0,
bool difficulty_sort = true) -> int;

// Match SolveBoard's remaining-trick count from remainCards alone.
auto remaining_tricks_from_holdings(
unsigned int const cards[DDS_HANDS][DDS_SUITS]) -> int
{
int card_count = 0;
for (int h = 0; h < DDS_HANDS; h++)
{
for (int s = 0; s < DDS_SUITS; s++)
card_count += count_table[cards[h][s] >> 2];
}

if (card_count % 4)
return ((card_count - 4) >> 2) + 2;
return ((card_count - 4) >> 2) + 1;
}

auto declarer_tricks_from_leader_score(
int remaining_tricks,
int leader_side_score) -> int
{
return remaining_tricks - leader_side_score;
}

namespace
{

// solve_same_board's null-window reuse assumes a full 13-trick deal.
constexpr int kFullDealRemainingTricks = 13;

auto is_full_thirteen_trick_deal(
unsigned int const cards[DDS_HANDS][DDS_SUITS]) -> bool
{
return remaining_tricks_from_holdings(cards) == kFullDealRemainingTricks;
}

} // namespace


auto calc_single_common_internal(
SolverContext& ctx,
Expand Down Expand Up @@ -63,13 +101,29 @@ auto calc_single_common_internal(
// for subsequent same-board solves to ensure all declarers on the same
// board share the same transposition table state, which is important
// for calculation consistency and fixes a previous consistency bug.
const bool reuse_same_board =
is_full_thirteen_trick_deal(deal.remainCards);
for (int k = 1; k < DDS_HANDS; k++)
{
int hint = (k == 2 ? fut.score[0] : 13 - fut.score[0]);

deal.first = k; // Next declarer

res = solve_same_board(ctx, deal, &fut, hint);
deal.first = k;
if (reuse_same_board)
{
// Fast path for full deals: null-window reuse with a partner/opponent hint.
const int hint =
(k == 2 ? fut.score[0] : kFullDealRemainingTricks - fut.score[0]);
res = solve_same_board(ctx, deal, &fut, hint);
}
else
{
// Partial deals: solve_same_board hints/reuse are incorrect; full solve.
res = solve_board(
ctx,
deal,
bds.target[bno],
bds.solutions[bno],
bds.mode[bno],
&fut);
}

if (res == 1)
solved.solved_board[bno].score[k] = fut.score[0];
Expand Down Expand Up @@ -223,6 +277,7 @@ int STDCALL CalcDDtableN(
if (res != 1)
return res;

const int tricks = remaining_tricks_from_holdings(tableDeal.cards);
for (int index = 0; index < DDS_STRAINS; index++)
{
int strain = bo.deals[index].trump;
Expand All @@ -232,7 +287,8 @@ int STDCALL CalcDDtableN(
for (int first = 0; first < DDS_HANDS; first++)
{
tablep->res_table[strain][ rho[first] ] =
13 - solved.solved_board[index].score[first];
declarer_tricks_from_leader_score(
tricks, solved.solved_board[index].score[first]);
}
}
return RETURN_NO_FAULT;
Expand Down Expand Up @@ -323,6 +379,7 @@ int STDCALL CalcAllTablesN(

for (int m = 0; m < dealsp->no_of_tables; m++)
{
const int tricks = remaining_tricks_from_holdings(dealsp->deals[m].cards);
for (int strainIndex = 0; strainIndex < count; strainIndex++)
{
int index = m * count + strainIndex;
Expand All @@ -333,7 +390,8 @@ int STDCALL CalcAllTablesN(
for (int first = 0; first < DDS_HANDS; first++)
{
resp->results[m].res_table[strain][ rho[first] ] =
13 - solved.solved_board[index].score[first];
declarer_tricks_from_leader_score(
tricks, solved.solved_board[index].score[first]);
}
}
}
Expand Down Expand Up @@ -414,11 +472,22 @@ auto calc_single_deal_scores(
return res;
scores[0] = fut.score[0];

const bool reuse_same_board =
is_full_thirteen_trick_deal(deal.remainCards);
for (int k = 1; k < DDS_HANDS; k++)
{
const int hint = (k == 2 ? fut.score[0] : 13 - fut.score[0]);
deal.first = k;
res = solve_same_board(ctx, deal, &fut, hint);
if (reuse_same_board)
{
const int hint =
(k == 2 ? fut.score[0] : kFullDealRemainingTricks - fut.score[0]);
res = solve_same_board(ctx, deal, &fut, hint);
}
else
{
// Partial deals: solve_same_board is wrong; use a full solve per leader.
res = solve_board(ctx, deal, target, solutions, mode, &fut);
}
if (res != RETURN_NO_FAULT)
return res;
scores[k] = fut.score[0];
Expand Down Expand Up @@ -528,14 +597,17 @@ int STDCALL CalcAllTablesX(

for (int m = 0; m < numDeals; m++)
{
const int tricks = remaining_tricks_from_holdings(deals[m].cards);
for (int strainIndex = 0; strainIndex < included; strainIndex++)
{
const int index = m * included + strainIndex;
const int strain = boards[static_cast<unsigned>(index)].trump;
for (int first = 0; first < DDS_HANDS; first++)
{
results[m].res_table[strain][rho[first]] =
13 - scores[static_cast<unsigned>(index)][static_cast<unsigned>(first)];
declarer_tricks_from_leader_score(
tricks,
scores[static_cast<unsigned>(index)][static_cast<unsigned>(first)]);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions library/src/calc_tables.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,11 @@ auto detect_calc_duplicates(
const Boards& bds,
std::vector<int>& uniques,
std::vector<int>& crossrefs) -> void;

// Match SolveBoard's remaining-trick count from remainCards alone.
auto remaining_tricks_from_holdings(
unsigned int const cards[DDS_HANDS][DDS_SUITS]) -> int;

auto declarer_tricks_from_leader_score(
int remaining_tricks,
int leader_side_score) -> int;
14 changes: 13 additions & 1 deletion library/src/solver_if.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -728,9 +728,21 @@ auto solve_same_board(

ctx.move_gen().reinit(trick, dl.first);

// ini_depth == cardCount - 4 (see solve_board). Bound the null-window
// search by remaining tricks so partial deals cannot report scores > 13
// leftovers from a full-hand upper bound.
const int card_count = ini_depth + 4;
const int remaining_tricks = (card_count % 4)
? ((card_count - 4) >> 2) + 2
: ((card_count - 4) >> 2) + 1;

int guess = hint;
if (guess < 0)
guess = 0;
if (guess > remaining_tricks)
guess = remaining_tricks;
int lowerbound = 0;
int upperbound = 13;
int upperbound = remaining_tricks;

do
{
Expand Down
11 changes: 11 additions & 0 deletions library/tests/system/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ cc_test(
],
)

cc_test(
name = "calc_dd_table_partial_test",
size = "small",
srcs = ["calc_dd_table_partial_test.cpp"],
deps = [
"//library/src:testable_dds",
"//library/src/api:api_definitions",
"@googletest//:gtest_main",
],
)

cc_test(
name = "worker_context_reuse_test",
size = "small",
Expand Down
109 changes: 109 additions & 0 deletions library/tests/system/calc_dd_table_partial_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/// @file calc_dd_table_partial_test.cpp
/// @brief CalcDDtable must report tricks out of remaining cards, not always 13.

#include <cstring>
#include <gtest/gtest.h>

#include <api/calc_dd_table.hpp>
#include <api/dll.h>

namespace
{

// One card each: NS hold ♠AK, EW hold ♠QJ. NS take the single trick as either
// declarer; EW take none.
constexpr const char* kOneTrickSpadesPbn = "N:A... Q... K... J...";

void expect_ns_take_all_remaining(const DdTableResults& table, int tricks)
{
for (int strain = 0; strain < DDS_STRAINS; strain++)
{
EXPECT_EQ(table.res_table[strain][0], tricks) << "strain=" << strain << " North";
EXPECT_EQ(table.res_table[strain][1], 0) << "strain=" << strain << " East";
EXPECT_EQ(table.res_table[strain][2], tricks) << "strain=" << strain << " South";
EXPECT_EQ(table.res_table[strain][3], 0) << "strain=" << strain << " West";
}
}

} // namespace

TEST(CalcDdTablePartial, OneCardPerHandUsesRemainingTricksNotThirteen)
{
InitializeStaticMemory();

DdTableDealPBN deal{};
std::strncpy(deal.cards, kOneTrickSpadesPbn, sizeof(deal.cards) - 1);
deal.cards[sizeof(deal.cards) - 1] = '\0';

DdTableResults table{};
ASSERT_EQ(CalcDDtablePBN(deal, &table), RETURN_NO_FAULT);

// Regression: before the fix, every entry was 13 or 12 (hardcoded 13 - score).
for (int strain = 0; strain < DDS_STRAINS; strain++)
for (int hand = 0; hand < DDS_HANDS; hand++)
{
EXPECT_GE(table.res_table[strain][hand], 0);
EXPECT_LE(table.res_table[strain][hand], 1)
<< "strain=" << strain << " hand=" << hand;
}

expect_ns_take_all_remaining(table, /*tricks=*/1);
}

TEST(CalcAllTablesPartial, OneCardPerHandUsesRemainingTricksNotThirteen)
{
InitializeStaticMemory();

DdTableDealsPBN deals{};
deals.no_of_tables = 1;
std::strncpy(deals.deals[0].cards, kOneTrickSpadesPbn, sizeof(deals.deals[0].cards) - 1);
deals.deals[0].cards[sizeof(deals.deals[0].cards) - 1] = '\0';

int trump_filter[DDS_STRAINS] = {0, 0, 0, 0, 0};
DdTablesRes resp{};
AllParResults par{};
ASSERT_EQ(
CalcAllTablesPBN(&deals, /*mode=*/-1, trump_filter, &resp, &par),
RETURN_NO_FAULT);

expect_ns_take_all_remaining(resp.results[0], /*tricks=*/1);
}

TEST(CalcAllTablesXPartial, OneCardPerHandUsesRemainingTricksNotThirteen)
{
InitializeStaticMemory();

DdTableDealPBN deal{};
std::strncpy(deal.cards, kOneTrickSpadesPbn, sizeof(deal.cards) - 1);
deal.cards[sizeof(deal.cards) - 1] = '\0';

int trump_filter[DDS_STRAINS] = {0, 0, 0, 0, 0};
DdTableResults result{};
ASSERT_EQ(
CalcAllTablesPBNX(1, &deal, /*mode=*/-1, trump_filter, &result, nullptr, 1),
RETURN_NO_FAULT);

expect_ns_take_all_remaining(result, /*tricks=*/1);
}

TEST(CalcDdTablePartialCpp, OneCardPerHandUsesRemainingTricksNotThirteen)
{
InitializeStaticMemory();

DdTableDealPBN deal_pbn{};
std::strncpy(deal_pbn.cards, kOneTrickSpadesPbn, sizeof(deal_pbn.cards) - 1);
deal_pbn.cards[sizeof(deal_pbn.cards) - 1] = '\0';

DdTableResults table{};
ASSERT_EQ(calc_dd_table_pbn(deal_pbn, &table), RETURN_NO_FAULT);

for (int strain = 0; strain < DDS_STRAINS; strain++)
for (int hand = 0; hand < DDS_HANDS; hand++)
{
EXPECT_GE(table.res_table[strain][hand], 0);
EXPECT_LE(table.res_table[strain][hand], 1)
<< "strain=" << strain << " hand=" << hand;
}

expect_ns_take_all_remaining(table, /*tricks=*/1);
}
Loading