Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f0634e1
Dispatch move-weight heuristics on the precomputed findex.
tameware Jul 23, 2026
93dd14c
Merge branch 'dds-bridge:develop' into fable-heuristic-inline
tameware Jul 23, 2026
a6e6d34
Fix findex encoding docs to match trump-winner semantics.
tameware Jul 23, 2026
cfc12ba
Require an explicit TrackType in make_heuristic_context.
tameware Jul 23, 2026
29730d0
Fall back to NT0 weights for invalid findex dispatch.
tameware Jul 23, 2026
a9524fb
Merge branch 'fable-heuristic-inline' of https://github.com/tameware/…
tameware Jul 23, 2026
f08a1e1
Initialize Moves fields before make_heuristic_context in tests.
tameware Jul 23, 2026
e7ad330
Store RelRanksType table statically in dispatch fixture tests.
tameware Jul 23, 2026
933d495
Guard out-of-range trump and take mutable HeuristicContext in call_he…
tameware Jul 23, 2026
631cb55
Initialize Moves scalars before hoisting heuristic context.
tameware Jul 23, 2026
3d3b62e
Document both call_heuristic overloads in the heuristic-sorting spec.
tameware Jul 23, 2026
358c1ca
Use a compact deal in MoveGen tests to avoid move-list overflow.
tameware Jul 23, 2026
e657e50
Potential fix for pull request finding
tameware Jul 23, 2026
d4e87f6
Snapshot only defined trick-card fields in make_heuristic_context.
tameware Jul 23, 2026
a6329c5
Bind HeuristicContext test refs to static storage.
tameware Jul 24, 2026
d87f771
Remove legacy call_heuristic overload that re-derives findex.
tameware Jul 24, 2026
bd361a8
Drop duplicate make_heuristic_context doxygen from the .cpp.
tameware Jul 24, 2026
4767a77
Replace heuristic findex with a WeightCase enum.
tameware Jul 24, 2026
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
83 changes: 24 additions & 59 deletions library/src/heuristic_sorting/heuristic_sorting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,31 @@
#include <utility/constants.h>
#include <lookup_tables/lookup_tables.hpp>

// New overload: accepts a pre-built HeuristicContext. This contains the
// same inline logic that used to be in the previous function body.
void call_heuristic(const HeuristicContext& context)
// The caller passes the weight case it already knows, so nothing is
// re-derived from the context here.
void call_heuristic(HeuristicContext& context, const WeightCase weight_case)
{
// Determine which position in trick (0=leading, 1-3=following)
int hand_rel = 0;
if (context.curr_hand != context.lead_hand) {
// Calculate relative position: 1, 2, or 3 based on lead hand
hand_rel = (context.curr_hand + 4 - context.lead_hand) % 4;
}

// Leading hand (hand_rel == 0) - MoveGen0 logic
if (hand_rel == 0) {
// Check if trump game with trump winner available
bool trump_game = (context.trump != DDS_NOTRUMP) &&
(context.trump >= 0 && context.trump < DDS_SUITS) &&
(context.tpos.winner[context.trump].rank != 0);

if (trump_game) {
weight_alloc_trump0(const_cast<HeuristicContext&>(context));
} else {
weight_alloc_nt0(const_cast<HeuristicContext&>(context));
}
return;
}

// Following hands (hand_rel 1-3) - MoveGen123 logic
// Check trump game condition
int ftest = ((context.trump != DDS_NOTRUMP) &&
(context.trump >= 0 && context.trump < DDS_SUITS) &&
(context.tpos.winner[context.trump].rank != 0) ? 1 : 0);

// Check if current hand can follow suit (not void)
unsigned short ris = context.tpos.rank_in_suit[context.curr_hand][context.lead_suit];
bool can_follow_suit = (ris != 0);

// Calculate function index using same logic as original
int findex;
if (can_follow_suit) {
findex = 4 * hand_rel + ftest;
} else {
findex = 4 * hand_rel + ftest + 2;
}

// Following hands function dispatch table (MoveGen123 logic)
switch (findex) {
case 4: weight_alloc_nt_notvoid1(const_cast<HeuristicContext&>(context)); break; // hand_rel=1, can follow, no trump
case 5: weight_alloc_trump_notvoid1(const_cast<HeuristicContext&>(context)); break; // hand_rel=1, can follow, trump
case 6: weight_alloc_nt_void1(const_cast<HeuristicContext&>(context)); break; // hand_rel=1, void, no trump
case 7: weight_alloc_trump_void1(const_cast<HeuristicContext&>(context)); break; // hand_rel=1, void, trump
case 8: weight_alloc_nt_notvoid2(const_cast<HeuristicContext&>(context)); break; // hand_rel=2, can follow, no trump
case 9: weight_alloc_trump_notvoid2(const_cast<HeuristicContext&>(context)); break; // hand_rel=2, can follow, trump
case 10: weight_alloc_nt_void2(const_cast<HeuristicContext&>(context)); break; // hand_rel=2, void, no trump
case 11: weight_alloc_trump_void2(const_cast<HeuristicContext&>(context)); break; // hand_rel=2, void, trump
case 12: weight_alloc_combined_notvoid3(const_cast<HeuristicContext&>(context)); break; // hand_rel=3, can follow, no trump
case 13: weight_alloc_combined_notvoid3(const_cast<HeuristicContext&>(context)); break; // hand_rel=3, can follow, trump
case 14: weight_alloc_nt_void3(const_cast<HeuristicContext&>(context)); break; // hand_rel=3, void, no trump
case 15: weight_alloc_trump_void3(const_cast<HeuristicContext&>(context)); break; // hand_rel=3, void, trump
default:
// Should not happen, but default to basic sorting
switch (weight_case) {
case WeightCase::Nt0: weight_alloc_nt0(context); break;
case WeightCase::Trump0: weight_alloc_trump0(context); break;
case WeightCase::NtNotVoid1: weight_alloc_nt_notvoid1(context); break;
case WeightCase::TrumpNotVoid1: weight_alloc_trump_notvoid1(context); break;
case WeightCase::NtVoid1: weight_alloc_nt_void1(context); break;
case WeightCase::TrumpVoid1: weight_alloc_trump_void1(context); break;
case WeightCase::NtNotVoid2: weight_alloc_nt_notvoid2(context); break;
case WeightCase::TrumpNotVoid2: weight_alloc_trump_notvoid2(context); break;
case WeightCase::NtVoid2: weight_alloc_nt_void2(context); break;
case WeightCase::TrumpVoid2: weight_alloc_trump_void2(context); break;
case WeightCase::CombinedNotVoid3:
case WeightCase::CombinedNotVoid3Trump:
weight_alloc_combined_notvoid3(context);
break;
case WeightCase::NtVoid3: weight_alloc_nt_void3(context); break;
case WeightCase::TrumpVoid3: weight_alloc_trump_void3(context); break;
default:
// Should not happen; fall back to NT leading weights so moves get
// deterministic ordering instead of stale/uninitialized weights.
weight_alloc_nt0(context);
break;
}
}
Expand Down
42 changes: 33 additions & 9 deletions library/src/heuristic_sorting/heuristic_sorting.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ struct HeuristicContext
int num_moves;
int last_num_moves;
const int trump;
const int suit; // For MoveGen0, the suit being considered
// For MoveGen0, the suit being considered. Mutable so callers can build
// the context once per move generation and update it per suit iteration.
int suit;
const TrackType* trackp;
const int curr_trick;
const int curr_hand;
Expand All @@ -60,18 +62,40 @@ struct HeuristicContext
int lead0_rank = 0; // trackp->move[0].rank
};

/// @brief Apply heuristic sorting to candidate moves in the given context.
/// @brief Which weight_alloc_* helper to run for the current move list.
///
/// Move generation already knows the lead/follow, trump-winner, and void
/// situation; it passes the matching case so call_heuristic does not re-derive
/// it from the context. Numeric values match the historical findex encoding
/// used by Moves::MoveGen0 / MoveGen123 (and DDS_MOVES RegisterList).
enum class WeightCase : int {
Nt0 = 0, ///< Leading hand, no trump winner available
Trump0 = 1, ///< Leading hand, trump winner available
NtNotVoid1 = 4, ///< 2nd hand, can follow, no trump winner
TrumpNotVoid1 = 5, ///< 2nd hand, can follow, trump winner
NtVoid1 = 6, ///< 2nd hand, void in lead suit, no trump winner
TrumpVoid1 = 7, ///< 2nd hand, void in lead suit, trump winner
NtNotVoid2 = 8, ///< 3rd hand, can follow, no trump winner
TrumpNotVoid2 = 9, ///< 3rd hand, can follow, trump winner
NtVoid2 = 10, ///< 3rd hand, void in lead suit, no trump winner
TrumpVoid2 = 11, ///< 3rd hand, void in lead suit, trump winner
CombinedNotVoid3 = 12, ///< 4th hand, can follow, no trump winner
CombinedNotVoid3Trump = 13,///< 4th hand, can follow, trump winner
NtVoid3 = 14, ///< 4th hand, void in lead suit, no trump winner
TrumpVoid3 = 15, ///< 4th hand, void in lead suit, trump winner
};

/// @brief Apply heuristic sorting using a precomputed weight case.
///
/// Evaluates candidate moves using position-dependent heuristics to assign
/// weights that guide search algorithms toward the most promising lines.
/// Heuristics vary based on position characteristics (leading/following hand,
/// trump presence, suit availability) to optimize move ordering efficiency.
/// Move generation already knows the dispatch case; passing it avoids
/// re-deriving the relative hand, trump state and voidness from the context
/// on every call.
///
/// @param context Pre-constructed HeuristicContext containing position data,
/// move arrays, and cached snapshots for efficient evaluation.
///
/// @note This function mutates the context's move weighting arrays.
/// @note Prefer this overload over parameterized versions to minimize
/// construction overhead in hot paths.
void call_heuristic(const HeuristicContext& context);
/// Non-const because weighting writes through context.mply.
/// @param weight_case Precomputed weight case (see WeightCase).
void call_heuristic(HeuristicContext& context, WeightCase weight_case);

150 changes: 110 additions & 40 deletions library/src/moves/moves.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,45 @@ const MgType RegisterList[16] = {MgType::NT0, MgType::TRUMP0,
#define MG_REGISTER(a, b) 1;
#endif

namespace
{

// Trump-winner bit of WeightCase: never index winner[trump]
// unless trump is a suit in [0, DDS_SUITS).
auto trump_winner_bit(const Pos& tpos, const int trump) -> int
{
return ((trump != DDS_NOTRUMP) &&
(trump >= 0 && trump < DDS_SUITS) &&
(tpos.winner[trump].rank != 0))
? 1
: 0;
}

// Encode following-hand WeightCase: 4 * hand_rel + trump_winner + (void ? 2 : 0).
auto weight_case_follow(const int hand_rel, const int trump_winner,
const bool is_void) -> WeightCase
{
return static_cast<WeightCase>(
4 * hand_rel + trump_winner + (is_void ? 2 : 0));
}

} // namespace

Moves::Moves() {
// Initialize non-owning pointers to nullptr for safety
trackp = nullptr;
mply = nullptr;

// Scalars snapshotted by make_heuristic_context must not be indeterminate.
leadHand = 0;
currHand = 0;
leadSuit = 0;
currTrick = 0;
trump = DDS_NOTRUMP;
suit = 0;
numMoves = 0;
lastNumMoves = 0;

funcName[static_cast<int>(MgType::NT0)] = "NT0";
funcName[static_cast<int>(MgType::TRUMP0)] = "Trump0";
funcName[static_cast<int>(MgType::NT_VOID1)] = "NT_Void1";
Expand Down Expand Up @@ -171,7 +205,21 @@ auto Moves::MoveGen0(const int tricks, const Pos &tpos,
mply = list.move;
for (int s = 0; s < DDS_SUITS; s++)
trackp->lowest_win[0][s] = 0;
// Reset fields snapshotted by make_heuristic_context before the hoist;
// suit/lastNumMoves are overwritten again before each call_heuristic.
numMoves = 0;
lastNumMoves = 0;
suit = 0;
leadSuit = 0;

// Leading-hand weight case, known here once instead of re-derived per
// suit inside the heuristic dispatcher.
const WeightCase lead_case = trump_winner_bit(tpos, trump)
? WeightCase::Trump0
: WeightCase::Nt0;
HeuristicContext hctx =
make_heuristic_context(tpos, bestMove, bestMoveTT, thrp_rel,
track[tricks]);

for (suit = 0; suit < DDS_SUITS; suit++) {
unsigned short ris = tpos.rank_in_suit[leadHand][suit];
Expand Down Expand Up @@ -202,12 +250,14 @@ auto Moves::MoveGen0(const int tricks, const Pos &tpos,
g--;
}

Moves::call_heuristic(tpos, bestMove, bestMoveTT, thrp_rel);
hctx.suit = suit;
hctx.last_num_moves = lastNumMoves;
hctx.num_moves = numMoves;
::call_heuristic(hctx, lead_case);
}

#ifdef DDS_MOVES
bool ftest = ((trump != DDS_NOTRUMP) && (tpos.winner[trump].rank != 0));
if (ftest)
if (lead_case == WeightCase::Trump0)
MG_REGISTER(MgType::TRUMP0, 0);
else
MG_REGISTER(MgType::NT0, 0);
Expand Down Expand Up @@ -237,11 +287,19 @@ auto Moves::MoveGen123(const int tricks, const int handRel, const Pos &tpos)

for (int s = 0; s < DDS_SUITS; s++)
trackp->lowest_win[handRel][s] = 0;
// Reset fields snapshotted by make_heuristic_context before either hoist.
// Follow-suit uses suit == leadSuit; the void path overwrites suit per
// iteration before call_heuristic.
numMoves = 0;
lastNumMoves = 0;
suit = leadSuit;

WeightCase weight_case;
const int trump_winner = trump_winner_bit(tpos, trump);

[[maybe_unused]] int findex;
int ftest =
((trump != DDS_NOTRUMP) && (tpos.winner[trump].rank != 0) ? 1 : 0);
// Empty best-move placeholders must outlive the hoisted context, which
// holds references to them.
const MoveType empty_move{};

unsigned short ris = tpos.rank_in_suit[currHand][leadSuit];

Expand All @@ -265,27 +323,35 @@ auto Moves::MoveGen123(const int tricks, const int handRel, const Pos &tpos)
g--;
}

findex = 4 * handRel + ftest;
weight_case = weight_case_follow(handRel, trump_winner, /*is_void=*/false);
#ifdef DDS_MOVES
MG_REGISTER(RegisterList[findex], handRel);
MG_REGISTER(RegisterList[static_cast<int>(weight_case)], handRel);
#endif

list.current = 0;
list.last = numMoves - 1;
if (numMoves == 1)
return numMoves;
Moves::call_heuristic(tpos, MoveType{}, MoveType{}, nullptr);

HeuristicContext hctx =
make_heuristic_context(tpos, empty_move, empty_move, nullptr,
track[tricks]);
::call_heuristic(hctx, weight_case);

Moves::MergeSort();
return numMoves;
}

findex = 4 * handRel + ftest + 2;
weight_case = weight_case_follow(handRel, trump_winner, /*is_void=*/true);

#ifdef DDS_MOVES
MG_REGISTER(RegisterList[findex], handRel);
MG_REGISTER(RegisterList[static_cast<int>(weight_case)], handRel);
#endif

HeuristicContext hctx =
make_heuristic_context(tpos, empty_move, empty_move, nullptr,
track[tricks]);

for (suit = 0; suit < DDS_SUITS; suit++) {
ris = tpos.rank_in_suit[currHand][suit];
if (ris == 0)
Expand All @@ -311,7 +377,10 @@ auto Moves::MoveGen123(const int tricks, const int handRel, const Pos &tpos)
g--;
}

Moves::call_heuristic(tpos, MoveType{}, MoveType{}, nullptr);
hctx.suit = suit;
hctx.last_num_moves = lastNumMoves;
hctx.num_moves = numMoves;
::call_heuristic(hctx, weight_case);
}

list.current = 0;
Expand Down Expand Up @@ -639,37 +708,38 @@ auto Moves::Sort(const int tricks, const int relHand) -> void {
mply[j] = tmp; \
}

/**
* @brief Build a heuristic context and invoke heuristic sorting.
*
* Centralizes the construction of HeuristicContext to keep the call sites
* consistent and avoid exposing mutable state to heuristic helpers.
*/
auto Moves::call_heuristic(const Pos &tpos, const MoveType &best_move,
const MoveType &best_move_tt,
const RelRanksType thrp_rel[]) -> void {
// Construct context once here and call the context-taking overload.
auto Moves::make_heuristic_context(const Pos &tpos, const MoveType &best_move,
const MoveType &best_move_tt,
const RelRanksType thrp_rel[],
const TrackType &tr) const
-> HeuristicContext {
HeuristicContext context{
tpos, best_move, best_move_tt, thrp_rel, mply, numMoves, lastNumMoves,
trump, suit, trackp, currTrick, currHand, leadHand, leadSuit};

// Snapshot removed_ranks into the context to avoid direct dependence on
// the mutable Moves::trackp buffer inside heuristic code.
for (int s = 0; s < DDS_SUITS; ++s) {
context.removed_ranks[s] = trackp ? trackp->removed_ranks[s] : 0;
trump, suit, &tr, currTrick, currHand, leadHand, leadSuit};

// Snapshot removed_ranks and only those trick-card fields that are defined
// for the current relative hand. Earlier slots may be unpopulated (MoveGen0
// / hand_rel 1–2), so leave the rest at HeuristicContext's zero defaults.
for (int s = 0; s < DDS_SUITS; ++s)
context.removed_ranks[s] = tr.removed_ranks[s];

const int hand_rel =
(currHand == leadHand) ? 0 : (currHand + 4 - leadHand) % 4;
if (hand_rel >= 1)
context.lead0_rank = tr.move[0].rank;
if (hand_rel >= 2)
{
context.move1_rank = tr.move[1].rank;
context.move1_suit = tr.move[1].suit;
context.high1 = tr.high[1];
}
if (hand_rel >= 3)
{
context.move2_rank = tr.move[2].rank;
context.move2_suit = tr.move[2].suit;
context.high2 = tr.high[2];
}
// Snapshot minimal trick state for helper usage.
context.move1_rank = (trackp ? trackp->move[1].rank : 0);
context.high1 = (trackp ? trackp->high[1] : 0);
context.move1_suit = (trackp ? trackp->move[1].suit : 0);
// Third-hand snapshots
context.move2_rank = (trackp ? trackp->move[2].rank : 0);
context.move2_suit = (trackp ? trackp->move[2].suit : 0);
context.high2 = (trackp ? trackp->high[2] : 0);
// Leader snapshot
context.lead0_rank = (trackp ? trackp->move[0].rank : 0);

::call_heuristic(context);
return context;
}

/**
Expand Down
Loading
Loading