refactor: extract apply_move_to_track() and add unit tests - #287
refactor: extract apply_move_to_track() and add unit tests#287wopdevries wants to merge 13 commits into
Conversation
75314b7 to
aa8fe22
Compare
|
All CI checks pass. Ready for review and merge when you have time. |
There was a problem hiding this comment.
Pull request overview
Refactors move-tracking state updates by extracting the shared logic from Moves::MakeSpecific, Moves::MakeNext, and Moves::MakeNextSimple into a single helper (apply_move_to_track()), and adds unit tests to validate key scenarios.
Changes:
- Added
Moves::apply_move_to_track()and rewiredMakeSpecific/MakeNext/MakeNextSimpleto use it. - Added 2 unit tests for
apply_move_to_track()covering lead-hand and follow-suit behavior. - Reduced duplicated code in
moves.cppby centralizing per-card track updates.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| library/src/moves/moves.cpp | Extracts and uses apply_move_to_track() for track updates across move-selection paths. |
| library/src/moves/moves.hpp | Declares the new helper in the Moves interface. |
| library/tests/moves/moves_test.cpp | Adds unit tests for the new track-update helper. |
|
I requested a Copilot review. I expect its second and third comments apply equally to the original code that you refactored. If so, you can address them in this PR to keep it as a pure refactor or add an issue to address them in the future - your choice. The two phrases in Copilot's third comment seem equivalent, so I asked Cursor for an opinion:
I won't have any other look at the PR myself until Copilot's comments are resolved. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
library/src/moves/moves.cpp:448
- apply_move_to_track() takes a trick index but relies on the caller to have already set trackp. That makes the API easy to misuse (trackp could point at a different trick than the provided trick), and in non-debug builds the assert won’t protect against null/incorrect trackp. Consider binding trackp from the trick argument inside this helper (and validating the trick index) so the precondition is enforced in one place.
auto Moves::apply_move_to_track(const MoveType &move, const int relHand,
const int trick) -> void {
assert(trackp != nullptr && "apply_move_to_track: trackp must be set");
assert(relHand >= 0 && relHand < DDS_HANDS);
if (relHand == 3)
library/src/moves/moves.hpp:347
- The docstring for apply_move_to_track() says trick is in (1..12), but other code/tests index tricks with 0..12 (arrays are sized [13]). This comment should match the actual allowed range and call out the additional constraint only when relHand==3 (since the helper writes track[trick - 1]).
* @brief Update TrackType state to reflect a played move.
*
* @param move Move to apply
* @param relHand Relative hand index within the current trick (0..3)
* @param trick Trick index (1..12); when relHand==3 updates next trick state
*/
library/tests/moves/moves_test.cpp:611
- The added unit tests cover leading and following suit, but they don’t exercise the two other important branches in apply_move_to_track(): (1) trump-played beats non-trump, and (2) relHand==3 completion updates track[trick - 1] (lead_hand and removed_ranks). Adding coverage for those branches would better lock in the refactor’s behavior and guard against regressions in MakeNext/MakeNextSimple.
TEST_F(MovesTest, ApplyMoveToTrackFollowSuit) {
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
moves->trackp = &moves->track[5];
|
First two new Copilot comments could be either addressed or postponed. Third seems relevant to this PR. |
|
I've asked for a new Copilot review. For now, I note that the PR description needs an update. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (4)
library/src/moves/moves.hpp:347
- The doc for apply_move_to_track() says
trickis 1..12, but the implementation/other APIs use 0..12 and only requiretrick > 0whenrelHand == 3(because it updatestrack[trick - 1]). This comment is currently misleading about valid values and the direction of the update.
* @brief Update TrackType state to reflect a played move.
*
* @param move Move to apply
* @param relHand Relative hand index within the current trick (0..3)
* @param trick Trick index (1..12); when relHand==3 updates next trick state
library/tests/moves/moves_test.cpp:668
- ApplyMoveToTrackTrickCompletion currently checks
removed_ranks[0] != 0, butremoved_ranksis typically non-zero immediately after Init(), so this assertion can pass even if apply_move_to_track() never updates removed ranks. Resetting removed_ranks to a known value before the trick and asserting other suits remain unchanged would make this test actually validate the behavior.
// removed_ranks should include all played cards
EXPECT_NE(moves->track[4].removed_ranks[0], 0);
library/src/moves/moves.cpp:447
- apply_move_to_track() takes a
trickparameter but still requires callers to pre-settrackp. Since all current callers settrackp = &track[trick], settingtrackpinside the helper would remove a fragile precondition (and make thetrickparam consistently used for all relHand values).
auto Moves::apply_move_to_track(const MoveType &move, const int relHand,
const int trick) -> void {
assert(trackp != nullptr && "apply_move_to_track: trackp must be set");
assert(relHand >= 0 && relHand < DDS_HANDS);
if (relHand == 3)
assert(trick > 0 && "apply_move_to_track: trick must be > 0 when relHand==3");
library/tests/moves/moves_test.cpp:589
- The PR description says "Added 2 unit tests for apply_move_to_track()", but this diff adds four new tests (LeadHand, FollowSuit, TrumpBeatsNonTrump, TrickCompletion). Please update the PR description to match what was actually added so the summary stays accurate.
TEST_F(MovesTest, ApplyMoveToTrackLeadHand) {
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
|
Regarding Copilot's comment on the PR description, I think it should actually be something like "Factored out duplicated code to apply_move_to_track() and added unit tests." The number of tests is unimportant. |
|
I see now! Copilot was referring to the detailed description, not the PR name itself. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (4)
library/tests/moves/moves_test.cpp:620
- In ApplyMoveToTrackFollowSuit (and the similar trump test), multiple assignments are packed onto one line. Elsewhere in this test file initializations use one statement per line; splitting these improves readability and keeps formatting consistent.
MoveType lead;
lead.suit = 0; lead.rank = 14; lead.sequence = 0;
moves->apply_move_to_track(lead, 0, 5);
// Follow with King of Spades — lower rank loses
library/tests/moves/moves_test.cpp:605
- PR description says “Added 2 unit tests for apply_move_to_track()”, but this file adds 4 new TEST_F cases for apply_move_to_track. Please update the PR description (or consolidate the tests) so it accurately reflects what was changed.
TEST_F(MovesTest, ApplyMoveToTrackLeadHand) {
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
moves->trackp = &moves->track[5];
MoveType move;
move.suit = 2; // Diamonds
move.rank = 10;
move.sequence = 0;
moves->apply_move_to_track(move, 0, 5);
EXPECT_EQ(moves->trackp->move[0].suit, 2);
EXPECT_EQ(moves->trackp->move[0].rank, 10);
EXPECT_EQ(moves->trackp->high[0], 0);
EXPECT_EQ(moves->trackp->lead_suit, 2);
EXPECT_EQ(moves->trackp->play_suits[0], 2);
EXPECT_EQ(moves->trackp->play_ranks[0], 10);
}
library/tests/moves/moves_test.cpp:674
- ApplyMoveToTrackTrickCompletion currently doesn’t strongly validate removed_ranks updates: getSampleRankInSuit() makes track[5].removed_ranks values non-deterministic for this scenario, and the test re-calls apply_move_to_track(relHand==3) after manually zeroing track[4].removed_ranks, which can pass even if the per-card OR logic is wrong. Making removed_ranks deterministic for the source track and asserting the expected bits are set after the first trick completion will make this test meaningful and stable.
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
moves->trackp = &moves->track[5];
moves->track[5].lead_hand = 0;
MoveType cards[4];
library/src/moves/moves.cpp:447
- apply_move_to_track() takes a trick index but still requires the caller to pre-set trackp. That makes the helper easy to misuse (null or pointing at the wrong track slot), and the debug-only assert won’t protect release builds. Consider making the helper self-contained by setting trackp from trick (and asserting trick is in-range) inside the function.
auto Moves::apply_move_to_track(const MoveType &move, const int relHand,
const int trick) -> void {
assert(trackp != nullptr && "apply_move_to_track: trackp must be set");
assert(relHand >= 0 && relHand < DDS_HANDS);
if (relHand == 3)
0585139 to
1c6bff2
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
library/src/moves/moves.cpp:447
- apply_move_to_track() relies on trackp being set by the caller, but Init() does not initialize trackp and the method signature already receives
trick. If trackp is unset or points to a different track entry thantrack[trick], this will assert in debug and can dereference a null/incorrect pointer in release builds (asserts compiled out). Make the helper self-contained by selectingtrack[trick]internally (and optionally assertingtrickbounds).
assert(trackp != nullptr && "apply_move_to_track: trackp must be set");
assert(relHand >= 0 && relHand < DDS_HANDS);
if (relHand == 3)
assert(trick > 0 && "apply_move_to_track: trick must be > 0 when relHand==3");
library/tests/moves/moves_test.cpp:680
- This test intends to verify that removed_ranks[0] contains the exact bits for A/K/Q/J, but the current assertion only checks for non-zero. That can pass even if the wrong bits are set. Compare against the expected bitmask so the test actually validates correctness.
// removed_ranks[0] (spades) should have bits set for A K Q J
EXPECT_NE(moves->track[4].removed_ranks[0], 0);
tameware
left a comment
There was a problem hiding this comment.
Looking good so far. Copilot's comments can seem never-ending. I've found they are worth addressing.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (5)
library/tests/moves/moves_test.cpp:608
- Use DDS_SUITS instead of a hard-coded
[4]in therankInSuitpointer type so the test stays consistent with the codebase constants and won’t break if suit count ever changes.
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
library/tests/moves/moves_test.cpp:633
- Use DDS_SUITS instead of a hard-coded
[4]in therankInSuitpointer type so the test stays consistent with the codebase constants and won’t break if suit count ever changes.
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
library/tests/moves/moves_test.cpp:658
- Use DDS_SUITS instead of a hard-coded
[4]in therankInSuitpointer type so the test stays consistent with the codebase constants and won’t break if suit count ever changes.
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
library/tests/moves/moves_test.cpp:588
- Use DDS_SUITS instead of a hard-coded
[4]in therankInSuitpointer type so the test stays consistent with the codebase constants and won’t break if suit count ever changes.
This issue also appears in the following locations of the same file:
- line 608
- line 633
- line 658
const unsigned short (*rankInSuit)[4] = getSampleRankInSuit();
library/tests/moves/moves_test.cpp:669
- Avoid hard-coded hand count constants in tests. Using DDS_HANDS here keeps the test aligned with the production constants and makes intent clearer.
MoveType cards[4];
for (int h = 0; h < 4; h++) {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (6)
library/tests/moves/moves_test.cpp:93
- This assertion/comment is incorrect:
DDS_NOTRUMPis 4 (seeconstants.h), sotrump == 3corresponds to Clubs. Use theDDS_NOTRUMPconstant to avoid silently testing the wrong strain.
EXPECT_EQ(moves->trump, 3); // 3 = notrump
// Verify move lists are reset
for (int h = 0; h < DDS_HANDS; h++) {
library/tests/moves/moves_test.cpp:153
Init(..., 3, ...)sets trump to Clubs, not notrump. UseDDS_NOTRUMPto ensure this test is exercising the intended no-trump path.
const unsigned short (*rankInSuit)[DDS_SUITS] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
library/tests/moves/moves_test.cpp:590
- This test initializes
trumpwith3, which is Clubs. If the intent is notrump, useDDS_NOTRUMP(4) so the test doesn't accidentally pass under a suit contract.
const unsigned short (*rankInSuit)[DDS_SUITS] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
moves->trackp = &moves->track[5];
library/tests/moves/moves_test.cpp:610
- This test initializes
trumpwith3, which is Clubs. If the intent is notrump, useDDS_NOTRUMP(4) to match the codebase’s strain encoding.
const unsigned short (*rankInSuit)[DDS_SUITS] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
moves->trackp = &moves->track[5];
library/tests/moves/moves_test.cpp:660
- This test initializes
trumpwith3, which is Clubs. If the intent is notrump, useDDS_NOTRUMP(4) so removed-ranks behavior is validated under the correct strain.
const unsigned short (*rankInSuit)[DDS_SUITS] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
moves->trackp = &moves->track[5];
library/tests/moves/moves_test.cpp:86
Init(..., 3, ...)sets trump to Clubs (3), not notrump. In this codebase, notrump isDDS_NOTRUMP(4), so this test is initializing the wrong contract strain.
This issue also appears in the following locations of the same file:
- line 90
- line 152
- line 588
- line 608
- line 658
const unsigned short (*rankInSuit)[DDS_SUITS] = getSampleRankInSuit();
moves->Init(5, 0, nullptr, nullptr, rankInSuit, 3, 0);
|
I'd be wary of the latest batch of Copilot comments. I've seen similar comments turn out to be incorrect when I ran them by Cursor in a context more familiar with our codebase. I'm not saying Copilot is wrong, just that it's worth verifying. |
|
Extracting what looks like copy-paste code into a re-usable method makes sense to me. I agree that copilot's last set of comments are dubious. |
Review from Claude Code:Review note: apply_move_to_track extraction silently changes MakeNextSimple What changed The extraction of apply_move_to_track (moves.cpp:442) gives MakeNextSimple a trick-completion side effect it did not have before. Previously, on Why it matters (this path is live) removed_ranks is seeded once by Init for the current trick (moves.cpp:161–172) and thereafter propagated forward on each trick completion rather All make_next_simple call sites pass hand_rel_first = (48 - ini_depth) % 4 (solver_if.cpp:151). When a position is solved with three cards Assessment This looks like a latent-bug fix, not a regression: the old MakeNextSimple was inconsistent with MakeNext, and the only observable difference The concern is purely that it's an unlabeled semantic change shipped inside a refactor, with no test over the relHand == 3 / hand_rel_first == 3 Proposed regression test Drives the change through the public MakeNextSimple entry point (not apply_move_to_track directly), so it locks in the behavior a caller actually Notes for the reviewer
|
Solver test suggested by ClaudeNote that it downgrades the problem from a possible trick counting bug to an inefficiency. This feels consistent with the empirical stability we have observed for the solver.
Follow-up: solver-level test for the hand_rel_first == 3 path — with a caveat worth reading first Before proposing an end-to-end test I traced where the propagated removed_ranks is actually read. All consumers (moves.cpp:232–243, 309–316, The consequence: alpha-beta's min-max result is invariant to removed_ranks. A stale value makes the generator under-merge equivalent cards That reframes finding #1: the omission in the old code was most likely an efficiency / equals-fidelity discrepancy on the 3-cards-on-trick path, Proposed test For the reviewer
|
Factored out duplicated code to
apply_move_to_track()and added unit tests.MakeSpecific,MakeNext, andMakeNextSimpleinto a single helper (128 lines → one function)