From ff9e3f63ce64cd787fa95db8c8580faa8197c4e0 Mon Sep 17 00:00:00 2001 From: HenryLee Date: Sun, 2 Aug 2026 19:47:00 +1000 Subject: [PATCH 1/4] Add split in state machine and bayes --- bayes/include/bayes.h | 3 + bayes/src/bayes.cpp | 25 +++- bayes/tests/CMakeLists.txt | 2 +- ...{bayes_tests.cpp => bayes_macau_tests.cpp} | 77 +++++++++--- legacy/EV/ProbabilityCalculator.cpp | 10 +- legacy/EV/Test.cpp | 4 +- state_machine/include/action.h | 1 + state_machine/include/hand.h | 12 +- state_machine/include/ruleset.h | 6 + state_machine/include/state.h | 8 ++ state_machine/include/state_machine.h | 15 ++- state_machine/src/state_machine.cpp | 119 +++++++++++++++--- state_machine/tests/state_machine_tests.cpp | 89 +++++++------ 13 files changed, 273 insertions(+), 98 deletions(-) rename bayes/tests/{bayes_tests.cpp => bayes_macau_tests.cpp} (51%) diff --git a/bayes/include/bayes.h b/bayes/include/bayes.h index e766917..2df33fa 100644 --- a/bayes/include/bayes.h +++ b/bayes/include/bayes.h @@ -22,4 +22,7 @@ ExpectedValue EvPlayerHits(const RuleSet& ruleset, PlayerHand playerHand, ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand); +ExpectedValue EvPlayerSplits(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand); + } // namespace BlackjackEngine::Bayes diff --git a/bayes/src/bayes.cpp b/bayes/src/bayes.cpp index 22eb4c6..c515783 100644 --- a/bayes/src/bayes.cpp +++ b/bayes/src/bayes.cpp @@ -65,6 +65,12 @@ ExpectedValue EvDoubles(const RuleSet& ruleset, const State& state) { }); } +// EV of splitting: split the hand into two and evaluate both +ExpectedValue EvSplits(const RuleSet& ruleset, const State& state) { + const auto& [first, second] = Split(ruleset, state); + return ExpectedValue(EvBest(ruleset, first).value + EvBest(ruleset, second).value); +} + // Value of playing a state optimally: a terminal state is simply scored, // otherwise it is the greatest expected value over every allowed action. This // drives both the player's choices and the dealer's forced play, since a @@ -87,31 +93,42 @@ ExpectedValue EvBest(const RuleSet& ruleset, const State& state) { if (IsAllowed(state.allowedActions, Action::Double)) best = std::max(best, EvDoubles(ruleset, state)); + if (IsAllowed(state.allowedActions, Action::Split)) + best = std::max(best, EvSplits(ruleset, state)); + return best; } ExpectedValue EvPlayerBestAction(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand) { return EvBest(ruleset, - InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); + InitializeState(ruleset, Turn::Player, playerHand, dealerHand)); } ExpectedValue EvPlayerStands(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand) { return EvStands(ruleset, - InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); + InitializeState(ruleset, Turn::Player, playerHand, dealerHand)); } ExpectedValue EvPlayerHits(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand) { return EvHits(ruleset, - InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); + InitializeState(ruleset, Turn::Player, playerHand, dealerHand)); } ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand) { return EvDoubles( - ruleset, InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); + ruleset, InitializeState(ruleset, Turn::Player, playerHand, dealerHand)); +} + +ExpectedValue EvPlayerSplits(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand) { + return EvSplits( + ruleset, + InitializeState(ruleset, Turn::Player, playerHand, dealerHand, + Action::Hit | Action::Stand | Action::Double | Action::Split)); } } // namespace BlackjackEngine::Bayes diff --git a/bayes/tests/CMakeLists.txt b/bayes/tests/CMakeLists.txt index 00e6c20..a666b9d 100644 --- a/bayes/tests/CMakeLists.txt +++ b/bayes/tests/CMakeLists.txt @@ -1,5 +1,5 @@ add_executable(bayes_tests - bayes_tests.cpp) + bayes_macau_tests.cpp) target_link_libraries(bayes_tests blackjack_bayes gtest_main) diff --git a/bayes/tests/bayes_tests.cpp b/bayes/tests/bayes_macau_tests.cpp similarity index 51% rename from bayes/tests/bayes_tests.cpp rename to bayes/tests/bayes_macau_tests.cpp index 91e89ec..2753880 100644 --- a/bayes/tests/bayes_tests.cpp +++ b/bayes/tests/bayes_macau_tests.cpp @@ -7,122 +7,159 @@ using namespace BlackjackEngine::Bayes; constexpr double Epsilon = 0.000001; +const RuleSet macauRule = { + .hitOnSoft17 = false, + .maxSplittedHands = 4, + .resplitAces = false, + .splitLosesOriginalToDealerBJ = false, + .doubleLosesOriginalToDealerBJ = true, + .doubleOnSplits = true, +}; + TEST(EvTests, TestPlayer16Dealer9Hit) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(16, false, 2), DealerHand(9)); + EvPlayerHits(macauRule, PlayerHand(16, false, 2), DealerHand(9)); EXPECT_NEAR(ev.value, -0.509322, Epsilon); } TEST(EvTests, TestPlayer16Dealer9Stand) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(16, false, 2), DealerHand(9)); + EvPlayerStands(macauRule, PlayerHand(16), DealerHand(9)); EXPECT_NEAR(ev.value, -0.543150, Epsilon); } TEST(EvTests, TestPlayer16Dealer9Best) { const ExpectedValue ev = - EvPlayerBestAction(RuleSet(), PlayerHand(16, false, 2), DealerHand(9)); + EvPlayerBestAction(macauRule, PlayerHand(16), DealerHand(9)); EXPECT_NEAR(ev.value, -0.509322, Epsilon); } TEST(EvTests, TestPlayer16Dealer10Hit) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(16, false, 2), DealerHand(10)); + EvPlayerHits(macauRule, PlayerHand(16), DealerHand(10)); EXPECT_NEAR(ev.value, -0.575224, Epsilon); } TEST(EvTests, TestPlayer16Dealer10Stand) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(16, false, 2), DealerHand(10)); + EvPlayerStands(macauRule, PlayerHand(16), DealerHand(10)); EXPECT_NEAR(ev.value, -0.575782, Epsilon); } TEST(EvTests, TestPlayer16Dealer10Best) { const ExpectedValue ev = - EvPlayerBestAction(RuleSet(), PlayerHand(16, false, 2), DealerHand(10)); + EvPlayerBestAction(macauRule, PlayerHand(16), DealerHand(10)); EXPECT_NEAR(ev.value, -0.575224, Epsilon); } TEST(EvTests, TestPlayer13Dealer6Hit) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(13, false, 2), DealerHand(6)); + EvPlayerHits(macauRule, PlayerHand(13), DealerHand(6)); EXPECT_NEAR(ev.value, -0.235626, Epsilon); } TEST(EvTests, TestPlayer13Dealer6Stand) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(13, false, 2), DealerHand(6)); + EvPlayerStands(macauRule, PlayerHand(13), DealerHand(6)); EXPECT_NEAR(ev.value, -0.153699, Epsilon); } TEST(EvTests, TestPlayer13Dealer6Best) { const ExpectedValue ev = - EvPlayerBestAction(RuleSet(), PlayerHand(13, false, 2), DealerHand(6)); + EvPlayerBestAction(macauRule, PlayerHand(13), DealerHand(6)); EXPECT_NEAR(ev.value, -0.153699, Epsilon); } TEST(EvTests, TestPlayer10Dealer6Double) { const ExpectedValue ev = - EvPlayerDoubles(RuleSet(), PlayerHand(10, false, 2), DealerHand(6)); + EvPlayerDoubles(macauRule, PlayerHand(10), DealerHand(6)); EXPECT_NEAR(ev.value, 0.575590, Epsilon); } TEST(EvTests, TestPlayer11Dealer7Double) { const ExpectedValue ev = - EvPlayerDoubles(RuleSet(), PlayerHand(11, false, 2), DealerHand(7)); + EvPlayerDoubles(macauRule, PlayerHand(11), DealerHand(7)); EXPECT_NEAR(ev.value, 0.462889, Epsilon); } TEST(EvTests, TestPlayer9DealerAceHit) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(9, false, 2), DealerHand(11, true)); + EvPlayerHits(macauRule, PlayerHand(9), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.353164, Epsilon); } TEST(EvTests, TestPlayer9DealerAceStand) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(9, false, 2), DealerHand(11, true)); + EvPlayerStands(macauRule, PlayerHand(9), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.769427, Epsilon); } TEST(EvTests, TestPlayer4DealerAceHit) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(4, false, 2), DealerHand(11, true)); + EvPlayerHits(macauRule, PlayerHand(4), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.482899, Epsilon); } TEST(EvTests, TestPlayer4DealerAceStand) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(4, false, 2), DealerHand(11, true)); + EvPlayerStands(macauRule, PlayerHand(4), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.769427, Epsilon); } TEST(EvTests, TestPlayerSoft16Dealer9Hit) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(16, true, 2), DealerHand(9)); + EvPlayerHits(macauRule, PlayerHand(16, true), DealerHand(9)); EXPECT_NEAR(ev.value, -0.148644, Epsilon); } TEST(EvTests, TestPlayerSoft16Dealer9Stands) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(16, true, 2), DealerHand(9)); + EvPlayerStands(macauRule, PlayerHand(16, true), DealerHand(9)); EXPECT_NEAR(ev.value, -0.543150, Epsilon); } TEST(EvTests, TestPlayerSoft16Dealer9Doubles) { const ExpectedValue ev = - EvPlayerDoubles(RuleSet(), PlayerHand(16, true, 2), DealerHand(9)); + EvPlayerDoubles(macauRule, PlayerHand(16, true), DealerHand(9)); EXPECT_NEAR(ev.value, -0.456367, Epsilon); } TEST(EvTests, TestPlayerSoft19DealerAceHits) { const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(19, true, 2), DealerHand(11, true)); + EvPlayerHits(macauRule, PlayerHand(19, true), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.311668, Epsilon); } TEST(EvTests, TestPlayerSoft19DealerAceStands) { const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(19, true, 2), DealerHand(11, true)); + EvPlayerStands(macauRule, PlayerHand(19, true), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.115483, Epsilon); } + +TEST(EvTests, TestPlayer16Dealer6Split) { + const ExpectedValue ev = + EvPlayerSplits(macauRule, PlayerHand(16), DealerHand(6)); + EXPECT_NEAR(ev.value, 0.412349, Epsilon); +} + +TEST(EvTests, TestPlayer16Dealer5Split) { + const ExpectedValue ev = + EvPlayerSplits(macauRule, PlayerHand(16), DealerHand(5)); + EXPECT_NEAR(ev.value, 0.299967, Epsilon); +} + +TEST(EvTests, TestPlayer20Dealer6Split) { + const ExpectedValue ev = + EvPlayerSplits(macauRule, PlayerHand(20), DealerHand(6)); + EXPECT_NEAR(ev.value, 0.575590, Epsilon); +} + +/* + * Commented out because it is too slow at the moment. + * We will introduce caches to fix this. +TEST(EvTests, TestPlayerSoft12Dealer9Split) { + const ExpectedValue ev = + EvPlayerSplits(macauRule, PlayerHand(12, true), DealerHand(9)); + EXPECT_NEAR(ev.value, 0.642847, Epsilon); +} + */ diff --git a/legacy/EV/ProbabilityCalculator.cpp b/legacy/EV/ProbabilityCalculator.cpp index 9d98d4b..cd07b56 100644 --- a/legacy/EV/ProbabilityCalculator.cpp +++ b/legacy/EV/ProbabilityCalculator.cpp @@ -24,18 +24,18 @@ void ProbabilityCalculator::ShowDeltaOfEachTakenCard(int iPlayerScore, ProbabilityCalculator::ProbabilityCalculator(void) { - iMaxTimesSplitted = 1; + iMaxTimesSplitted = 4; bResplitAces = false; bHitOnSoft17 = false; - bDOBO = true; + bDOBO = false; bSOBO = false; iDeckNumber = 6; - bDoubleOnAnyTwo = false; - bDoubleOnSoft = false; + bDoubleOnAnyTwo = true; + bDoubleOnSoft = true; bDoubleOnNine = true; bDoubleOnTen = true; bDoubleOnEleven = true; - bStandAfterSplittedAces = true; + bStandAfterSplittedAces = false; } ProbabilityCalculator::~ProbabilityCalculator(void) diff --git a/legacy/EV/Test.cpp b/legacy/EV/Test.cpp index ff0845f..dd0fbd4 100644 --- a/legacy/EV/Test.cpp +++ b/legacy/EV/Test.cpp @@ -34,8 +34,8 @@ enum ActionAllowed void EvMain(string arg) { - ProbabilityCalculator * cal = new AdvancedCalculator; -// ProbabilityCalculator * cal = new SimpleFastCalculator; +// ProbabilityCalculator * cal = new AdvancedCalculator; + ProbabilityCalculator * cal = new SimpleFastCalculator; // ProbabilityCalculator * cal = new AdvancedFastCalculator; UsedCard current = usedcard; string sPScore; diff --git a/state_machine/include/action.h b/state_machine/include/action.h index 6639efb..4af8af9 100644 --- a/state_machine/include/action.h +++ b/state_machine/include/action.h @@ -12,6 +12,7 @@ enum class Action : unsigned char { Hit = 1u << 0, Stand = 1u << 1, Double = 1u << 2, + Split = 1u << 3, }; constexpr Action operator|(Action a, Action b) { diff --git a/state_machine/include/hand.h b/state_machine/include/hand.h index b243083..4c715c4 100644 --- a/state_machine/include/hand.h +++ b/state_machine/include/hand.h @@ -6,9 +6,9 @@ namespace BlackjackEngine::StateMachine { struct Hand { - unsigned char score = 0; + unsigned score = 0; bool isSoft = false; - unsigned char numCards = 0; + unsigned numCards = 0; explicit constexpr Hand(unsigned char v) : score(v), numCards(1) {} constexpr Hand(unsigned char v, bool soft) @@ -44,18 +44,14 @@ struct Hand { struct DealerHand : public Hand { constexpr DealerHand(const DealerHand& hand) = default; constexpr DealerHand(const Hand& hand) : Hand(hand) {} - explicit constexpr DealerHand(unsigned char v) : Hand(v) {} - constexpr DealerHand(unsigned char v, bool soft) : Hand(v, soft) {} - constexpr DealerHand(unsigned char v, bool soft, unsigned char cards) + explicit constexpr DealerHand(unsigned char v, bool soft = false, unsigned char cards = 1) : Hand(v, soft, cards) {} }; struct PlayerHand : public Hand { constexpr PlayerHand(const PlayerHand& hand) = default; constexpr PlayerHand(const Hand& hand) : Hand(hand) {} - explicit constexpr PlayerHand(unsigned char v) : Hand(v) {} - constexpr PlayerHand(unsigned char v, bool soft) : Hand(v, soft) {} - constexpr PlayerHand(unsigned char v, bool soft, unsigned char cards) + explicit constexpr PlayerHand(unsigned char v, bool soft = false, unsigned char cards = 2) : Hand(v, soft, cards) {} }; diff --git a/state_machine/include/ruleset.h b/state_machine/include/ruleset.h index 5d1a082..b1b2d34 100644 --- a/state_machine/include/ruleset.h +++ b/state_machine/include/ruleset.h @@ -5,6 +5,12 @@ namespace BlackjackEngine::StateMachine { struct RuleSet { bool hitOnSoft17 = false; + unsigned maxSplittedHands = 4; + bool resplitAces = true; + bool standOnSplitAces = false; + bool splitLosesOriginalToDealerBJ = false; + bool doubleLosesOriginalToDealerBJ = false; + bool doubleOnSplits = true; }; } // namespace BlackjackEngine::StateMachine diff --git a/state_machine/include/state.h b/state_machine/include/state.h index 1d74b3a..5ddf847 100644 --- a/state_machine/include/state.h +++ b/state_machine/include/state.h @@ -19,6 +19,14 @@ struct State { DealerHand dealerHand; Action allowedActions; + // How many times this hand's lineage has been split (0 = original hand). + unsigned splitDepth = 0; + + // The score of the single card after a split (e.g. 8 for a split pair of 8s). + // Used to determine re-split eligibility: only when the drawn second card + // matches this value can the hand be split again. + unsigned splitCardScore = 0; + constexpr State(Turn turn, PlayerHand playerHand, DealerHand dealerHand, Action allowedActions = Action::Hit | Action::Stand | Action::Double) diff --git a/state_machine/include/state_machine.h b/state_machine/include/state_machine.h index 98a66a5..22e119d 100644 --- a/state_machine/include/state_machine.h +++ b/state_machine/include/state_machine.h @@ -5,6 +5,8 @@ #include "ruleset.h" #include "state.h" +#include + namespace BlackjackEngine::StateMachine { // The result of a finished round, from the player's perspective. @@ -32,10 +34,10 @@ Outcome Result(const State& state); // the double requirement is met (it is the player's turn holding exactly two // cards). A dealer-turn state is advanced straight to the dealer's forced // state. -State InitiateState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, - DealerHand dealerHand, - Action allowedActions = Action::Hit | Action::Stand | - Action::Double); +State InitializeState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, + DealerHand dealerHand, + Action allowedActions = Action::Hit | Action::Stand | + Action::Double); // Stand: the acting party takes no more cards. On the player's turn play passes // to the dealer, who is advanced to their next forced state; on the dealer's @@ -53,6 +55,11 @@ State Hit(const RuleSet& ruleset, const State& state, Card card); // allowed to double. State Double(const State& state, Card card); +// Split: split the two same value cards into two different hands. +// Currently only the player is allowed to split. +// Only allowed if the player hand has two cards with exactly the same value. +std::pair Split(const RuleSet& ruleset, const State& state); + } // namespace BlackjackEngine::StateMachine #endif // STATE_MACHINE_H diff --git a/state_machine/src/state_machine.cpp b/state_machine/src/state_machine.cpp index ddd47dd..942342a 100644 --- a/state_machine/src/state_machine.cpp +++ b/state_machine/src/state_machine.cpp @@ -17,11 +17,74 @@ bool DealerShouldHit(const RuleSet& ruleset, const State& state) { return hand.isSoft && ruleset.hitOnSoft17; } +// Advance a state by recalculate the allowed actions for player. +// Used internally by Hit and Split. Double doesn't need to come here +// since Stand is the only allowed action after doubling. +State EnterPlayer(const RuleSet& ruleset, const State& state) { + assert(state.turn == Turn::Player); + + Action actions = state.allowedActions; + + if (state.playerHand.numCards < 2) { + actions &= ~Action::Stand; + actions |= Action::Hit; + + } else { + actions |= Action::Stand; + } + + if (state.playerHand.numCards != 2) { + actions &= ~(Action::Double | Action::Split); + } else { + // Stand on split aces + if (state.splitCardScore == 11 && ruleset.standOnSplitAces) { + actions &= ~(Action::Hit | Action::Double | Action::Split); + + // Still allow re-split if aces are drawn and resplitAces is enabled. + if (ruleset.resplitAces && state.splitDepth < ruleset.maxSplittedHands && + state.playerHand.score == 12 && state.playerHand.isSoft) { + actions |= Action::Split; + } + } else { + // Re-enable Double for split hands that just received their second card. + if (state.splitDepth > 0 && ruleset.doubleOnSplits) { + actions |= Action::Double; + } + + // Re-enable Split only when the drawn card matches the original split card. + // For non-aces: hand score == splitCardScore * 2. + // For aces (splitCardScore == 11): hand is soft 12 (11 + 11 wraps). + bool drawnCardMatchesSplit = + state.splitCardScore > 0 && + (state.playerHand.score == state.splitCardScore * 2 || + (state.splitCardScore == 11 && ruleset.resplitAces && + state.playerHand.score == 12 && state.playerHand.isSoft)); + if (state.splitDepth > 0 && state.splitDepth < ruleset.maxSplittedHands && + drawnCardMatchesSplit) { + actions |= Action::Split; + } + + if (state.playerHand.score % 2 != 0) { + actions &= ~Action::Split; + } else if (state.playerHand.isSoft && state.playerHand.score != 12) { + actions &= ~Action::Split; + } + } + } + + State next = state; + next.allowedActions = actions; + + return next; +} + // Advance a state into the dealer's turn: the dealer keeps hitting while the // ruleset requires it, so a dealer-turn state always has Hit as its only // allowed action. Once the dealer must stand, the round is finished. Used -// internally by Stand, Hit, and InitiateState. +// internally by Stand, Hit, and InitializeState. State EnterDealer(const RuleSet& ruleset, State state) { + assert(state.turn <= Turn::Dealer); + if (DealerShouldHit(ruleset, state)) { state.turn = Turn::Dealer; state.allowedActions = Action::Hit; @@ -47,7 +110,7 @@ Outcome Result(const State& state) { // Blackjacks (a two-card 21) are settled before comparing totals: two // blackjacks push, otherwise the side holding the blackjack wins. - const bool playerBlackjack = player.numCards == 2 && player.score == 21; + const bool playerBlackjack = player.numCards == 2 && player.score == 21 && state.splitDepth == 0; const bool dealerBlackjack = dealer.numCards == 2 && dealer.score == 21; if (playerBlackjack || dealerBlackjack) { @@ -63,16 +126,14 @@ Outcome Result(const State& state) { return Outcome::Push; } -State InitiateState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, +State InitializeState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, DealerHand dealerHand, Action allowedActions) { - if (turn != Turn::Player || playerHand.numCards != 2) - allowedActions &= ~Action::Double; - - State state(turn, playerHand, dealerHand, allowedActions); + const State state(turn, playerHand, dealerHand, allowedActions); - if (turn == Turn::Dealer) return EnterDealer(ruleset, state); - - return state; + if (turn == Turn::Player) + return EnterPlayer(ruleset, state); + else + return EnterDealer(ruleset, state); } State Stand(const RuleSet& ruleset, const State& state) { @@ -96,14 +157,15 @@ State Hit(const RuleSet& ruleset, const State& state, Card card) { } next.playerHand = next.playerHand + card; - next.allowedActions &= ~Action::Double; if (next.playerHand.score > 21) { next.turn = Turn::End; next.allowedActions = Action::None; - } - return next; + return next; + } else { + return EnterPlayer(ruleset, next); + } } State Double(const State& state, Card card) { @@ -112,8 +174,37 @@ State Double(const State& state, Card card) { State next = state; next.playerHand = next.playerHand + card; - next.allowedActions &= ~(Action::Hit | Action::Double); + next.allowedActions &= ~(Action::Hit | Action::Double | Action::Split); return next; } +std::pair Split(const RuleSet& ruleset, const State& state) { + assert(state.turn == Turn::Player); + assert(IsAllowed(state.allowedActions, Action::Split)); + assert(state.playerHand.numCards == 2); + assert(state.playerHand.score % 2 == 0); + assert(!state.playerHand.isSoft || state.playerHand.score == 12); + + State first = state; + + if (state.playerHand.score == 12 && state.playerHand.isSoft) { + // two aces + first.playerHand.score = 11; + + if (!ruleset.resplitAces) { + first.allowedActions &= ~Action::Split; + } + } else { + first.playerHand.score = state.playerHand.score / 2; + } + + first.playerHand.numCards = 1; + first.splitDepth = state.splitDepth + 1; + first.splitCardScore = first.playerHand.score; + + State second = first; + + return {EnterPlayer(ruleset, first), EnterPlayer(ruleset, second)}; +} + } // namespace BlackjackEngine::StateMachine diff --git a/state_machine/tests/state_machine_tests.cpp b/state_machine/tests/state_machine_tests.cpp index a7857dc..5ce0fd5 100644 --- a/state_machine/tests/state_machine_tests.cpp +++ b/state_machine/tests/state_machine_tests.cpp @@ -16,34 +16,34 @@ State DealerTurn(PlayerHand player, DealerHand dealer) { } // namespace -TEST(StateMachineTests, InitiateStateKeepsDoubleOnTwoCards) { - const State state = InitiateState( - RuleSet(), Turn::Player, PlayerHand(10, false, 2), - DealerHand(6, false, 1), Action::Hit | Action::Stand | Action::Double); +TEST(StateMachineTests, InitializeStateKeepsDoubleOnTwoCards) { + const State state = InitializeState( + RuleSet(), Turn::Player, PlayerHand(10), + DealerHand(6), Action::Hit | Action::Stand | Action::Double); EXPECT_EQ(state.turn, Turn::Player); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Double)); } -TEST(StateMachineTests, InitiateStateRemovesDoubleWhenNotTwoCards) { - const State state = InitiateState( +TEST(StateMachineTests, InitializeStateRemovesDoubleWhenNotTwoCards) { + const State state = InitializeState( RuleSet(), Turn::Player, PlayerHand(15, false, 3), - DealerHand(6, false, 1), Action::Hit | Action::Stand | Action::Double); + DealerHand(6), Action::Hit | Action::Stand | Action::Double); EXPECT_FALSE(IsAllowed(state.allowedActions, Action::Double)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Stand)); } -TEST(StateMachineTests, InitiateStateDealerEntersForcedHit) { - const State state = InitiateState( - RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - DealerHand(9, false, 1), Action::Hit | Action::Stand | Action::Double); +TEST(StateMachineTests, InitializeStateDealerEntersForcedHit) { + const State state = InitializeState( + RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(9), Action::Hit | Action::Stand | Action::Double); EXPECT_EQ(state.turn, Turn::Dealer); EXPECT_EQ(state.allowedActions, Action::Hit); } -TEST(StateMachineTests, InitiateStateDealerStandBecomesTerminal) { - const State state = InitiateState( - RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), +TEST(StateMachineTests, InitializeStateDealerStandBecomesTerminal) { + const State state = InitializeState( + RuleSet(), Turn::Dealer, PlayerHand(18), DealerHand(19, false, 2), Action::Hit | Action::Stand | Action::Double); EXPECT_EQ(state.turn, Turn::End); EXPECT_EQ(state.allowedActions, Action::None); @@ -51,14 +51,14 @@ TEST(StateMachineTests, InitiateStateDealerStandBecomesTerminal) { TEST(StateMachineTests, StandMovesToDealer) { const State next = Stand( - RuleSet(), PlayerTurn(PlayerHand(18, false, 2), DealerHand(9, false, 1))); + RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(9))); EXPECT_EQ(next.turn, Turn::Dealer); EXPECT_EQ(next.playerHand.score, 18); } TEST(StateMachineTests, HitStaysWithPlayerWhenNoBust) { const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(12, false, 2), DealerHand(9, false, 1)), + RuleSet(), PlayerTurn(PlayerHand(12), DealerHand(9)), Card(5)); EXPECT_EQ(next.turn, Turn::Player); EXPECT_EQ(next.playerHand.score, 17); @@ -67,7 +67,7 @@ TEST(StateMachineTests, HitStaysWithPlayerWhenNoBust) { TEST(StateMachineTests, HitBustEndsRound) { const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(16, false, 2), DealerHand(9, false, 1)), + RuleSet(), PlayerTurn(PlayerHand(16), DealerHand(9)), Card(10)); EXPECT_EQ(next.turn, Turn::End); EXPECT_EQ(next.playerHand.score, 26); @@ -77,7 +77,7 @@ TEST(StateMachineTests, HitBustEndsRound) { TEST(StateMachineTests, DoubleAddsCardAndLeavesOnlyStand) { const State next = Double( - PlayerTurn(PlayerHand(10, false, 2), DealerHand(6, false, 1)), Card(9)); + PlayerTurn(PlayerHand(10), DealerHand(6)), Card(9)); EXPECT_EQ(next.turn, Turn::Player); EXPECT_EQ(next.playerHand.score, 19); EXPECT_EQ(next.playerHand.numCards, 3); @@ -88,46 +88,46 @@ TEST(StateMachineTests, DoubleAddsCardAndLeavesOnlyStand) { TEST(StateMachineTests, DealerHitsBelow17) { const State state = - InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - DealerHand(16, false, 2)); + InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(16, false, 2)); EXPECT_FALSE(IsTerminal(state)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); } TEST(StateMachineTests, DealerStandsOnHard17) { const State state = - InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - DealerHand(17, false, 2)); + InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(17, false, 2)); EXPECT_TRUE(IsTerminal(state)); } TEST(StateMachineTests, DealerStandsAbove17) { const State state = - InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - DealerHand(19, false, 2)); + InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(19, false, 2)); EXPECT_TRUE(IsTerminal(state)); } TEST(StateMachineTests, DealerStandsOnSoft17WhenRuleOff) { RuleSet ruleset; ruleset.hitOnSoft17 = false; - const State state = InitiateState( - ruleset, Turn::Dealer, PlayerHand(18, false, 2), DealerHand(17, true, 2)); + const State state = InitializeState( + ruleset, Turn::Dealer, PlayerHand(18), DealerHand(17, true, 2)); EXPECT_TRUE(IsTerminal(state)); } TEST(StateMachineTests, DealerHitsOnSoft17WhenRuleOn) { RuleSet ruleset; ruleset.hitOnSoft17 = true; - const State state = InitiateState( - ruleset, Turn::Dealer, PlayerHand(18, false, 2), DealerHand(17, true, 2)); + const State state = InitializeState( + ruleset, Turn::Dealer, PlayerHand(18), DealerHand(17, true, 2)); EXPECT_FALSE(IsTerminal(state)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); } TEST(StateMachineTests, HitAddsToDealerOnDealerTurn) { const State next = Hit( - RuleSet(), DealerTurn(PlayerHand(18, false, 2), DealerHand(10, false, 1)), + RuleSet(), DealerTurn(PlayerHand(18), DealerHand(10)), Card(5)); EXPECT_EQ(next.dealerHand.score, 15); EXPECT_EQ(next.dealerHand.numCards, 2); @@ -136,14 +136,14 @@ TEST(StateMachineTests, HitAddsToDealerOnDealerTurn) { } TEST(StateMachineTests, StandEndsRoundOnDealerTurn) { - const State next = Stand(RuleSet(), DealerTurn(PlayerHand(18, false, 2), + const State next = Stand(RuleSet(), DealerTurn(PlayerHand(18), DealerHand(19, false, 2))); EXPECT_EQ(next.turn, Turn::End); EXPECT_TRUE(IsTerminal(next)); } TEST(StateMachineTests, ResultDealerBustIsPlayerWin) { - const State state(Turn::End, PlayerHand(18, false, 2), + const State state(Turn::End, PlayerHand(18), DealerHand(23, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } @@ -155,13 +155,13 @@ TEST(StateMachineTests, ResultDealerBlackjackBeatsPlayer) { } TEST(StateMachineTests, ResultBothBlackjacksPush) { - const State state(Turn::End, PlayerHand(21, false, 2), + const State state(Turn::End, PlayerHand(21), DealerHand(21, false, 2)); EXPECT_EQ(Result(state), Outcome::Push); } TEST(StateMachineTests, ResultPlayerBlackjackBeatsNonBlackjack) { - const State state(Turn::End, PlayerHand(21, false, 2), + const State state(Turn::End, PlayerHand(21), DealerHand(21, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } @@ -192,7 +192,7 @@ TEST(StateMachineTests, ResultEqualIsPush) { TEST(StateMachineTests, InitialPlayerStateAllowsAllActions) { const State state = - PlayerTurn(PlayerHand(10, false, 2), DealerHand(6, false, 1)); + PlayerTurn(PlayerHand(10), DealerHand(6)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Stand)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Double)); @@ -200,7 +200,7 @@ TEST(StateMachineTests, InitialPlayerStateAllowsAllActions) { TEST(StateMachineTests, HitRemovesDoubleFromAllowedActions) { const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(12, false, 2), DealerHand(6, false, 1)), + RuleSet(), PlayerTurn(PlayerHand(12), DealerHand(6)), Card(3)); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Stand)); @@ -209,20 +209,20 @@ TEST(StateMachineTests, HitRemovesDoubleFromAllowedActions) { TEST(StateMachineTests, BustLeavesNoAllowedActions) { const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(16, false, 2), DealerHand(6, false, 1)), + RuleSet(), PlayerTurn(PlayerHand(16), DealerHand(6)), Card(10)); EXPECT_EQ(next.allowedActions, Action::None); } TEST(StateMachineTests, StandSetsDealerForcedHit) { const State next = Stand( - RuleSet(), PlayerTurn(PlayerHand(18, false, 2), DealerHand(9, false, 1))); + RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(9))); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); EXPECT_FALSE(IsAllowed(next.allowedActions, Action::Stand)); } TEST(StateMachineTests, StandEndsRoundWhenDealerMustStand) { - const State next = Stand(RuleSet(), PlayerTurn(PlayerHand(18, false, 2), + const State next = Stand(RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(19, false, 2))); EXPECT_EQ(next.turn, Turn::End); EXPECT_TRUE(IsTerminal(next)); @@ -233,14 +233,23 @@ TEST(StateMachineTests, DealerForcedActionRespectsHitOnSoft17) { RuleSet ruleset; ruleset.hitOnSoft17 = true; const State next = Stand( - ruleset, PlayerTurn(PlayerHand(18, false, 2), DealerHand(17, true, 2))); + ruleset, PlayerTurn(PlayerHand(18), DealerHand(17, true, 2))); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); } TEST(StateMachineTests, StandAfterDoubleMovesToDealer) { const State doubled = Double( - PlayerTurn(PlayerHand(10, false, 2), DealerHand(9, false, 1)), Card(9)); + PlayerTurn(PlayerHand(10), DealerHand(9)), Card(9)); const State next = Stand(RuleSet(), doubled); EXPECT_EQ(next.turn, Turn::Dealer); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); } + +TEST(StateMachineTests, SplitIncrementsSplitDepth) { + State state(Turn::Player, PlayerHand(16), DealerHand(9), + Action::Hit | Action::Stand | Action::Double | Action::Split); + const auto& [first, second] = Split(RuleSet(), state); + + EXPECT_EQ(first.splitDepth, 1); + EXPECT_EQ(second.splitDepth, 1); +} From e3860fccfbbe1294da0cfcd558baf0bebfb1cec7 Mon Sep 17 00:00:00 2001 From: HenryLee Date: Sun, 2 Aug 2026 19:49:08 +1000 Subject: [PATCH 2/4] Fix clang format --- bayes/src/bayes.cpp | 15 +-- bayes/tests/bayes_macau_tests.cpp | 12 +-- state_machine/include/hand.h | 6 +- state_machine/include/state_machine.h | 4 +- state_machine/src/state_machine.cpp | 11 +- state_machine/tests/state_machine_tests.cpp | 107 +++++++++----------- 6 files changed, 73 insertions(+), 82 deletions(-) diff --git a/bayes/src/bayes.cpp b/bayes/src/bayes.cpp index c515783..e52d3d3 100644 --- a/bayes/src/bayes.cpp +++ b/bayes/src/bayes.cpp @@ -68,7 +68,8 @@ ExpectedValue EvDoubles(const RuleSet& ruleset, const State& state) { // EV of splitting: split the hand into two and evaluate both ExpectedValue EvSplits(const RuleSet& ruleset, const State& state) { const auto& [first, second] = Split(ruleset, state); - return ExpectedValue(EvBest(ruleset, first).value + EvBest(ruleset, second).value); + return ExpectedValue(EvBest(ruleset, first).value + + EvBest(ruleset, second).value); } // Value of playing a state optimally: a terminal state is simply scored, @@ -107,8 +108,8 @@ ExpectedValue EvPlayerBestAction(const RuleSet& ruleset, PlayerHand playerHand, ExpectedValue EvPlayerStands(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand) { - return EvStands(ruleset, - InitializeState(ruleset, Turn::Player, playerHand, dealerHand)); + return EvStands( + ruleset, InitializeState(ruleset, Turn::Player, playerHand, dealerHand)); } ExpectedValue EvPlayerHits(const RuleSet& ruleset, PlayerHand playerHand, @@ -125,10 +126,10 @@ ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, PlayerHand playerHand, ExpectedValue EvPlayerSplits(const RuleSet& ruleset, PlayerHand playerHand, DealerHand dealerHand) { - return EvSplits( - ruleset, - InitializeState(ruleset, Turn::Player, playerHand, dealerHand, - Action::Hit | Action::Stand | Action::Double | Action::Split)); + return EvSplits(ruleset, + InitializeState(ruleset, Turn::Player, playerHand, dealerHand, + Action::Hit | Action::Stand | Action::Double | + Action::Split)); } } // namespace BlackjackEngine::Bayes diff --git a/bayes/tests/bayes_macau_tests.cpp b/bayes/tests/bayes_macau_tests.cpp index 2753880..3fad46a 100644 --- a/bayes/tests/bayes_macau_tests.cpp +++ b/bayes/tests/bayes_macau_tests.cpp @@ -8,12 +8,12 @@ using namespace BlackjackEngine::Bayes; constexpr double Epsilon = 0.000001; const RuleSet macauRule = { - .hitOnSoft17 = false, - .maxSplittedHands = 4, - .resplitAces = false, - .splitLosesOriginalToDealerBJ = false, - .doubleLosesOriginalToDealerBJ = true, - .doubleOnSplits = true, + .hitOnSoft17 = false, + .maxSplittedHands = 4, + .resplitAces = false, + .splitLosesOriginalToDealerBJ = false, + .doubleLosesOriginalToDealerBJ = true, + .doubleOnSplits = true, }; TEST(EvTests, TestPlayer16Dealer9Hit) { diff --git a/state_machine/include/hand.h b/state_machine/include/hand.h index 4c715c4..9d570a3 100644 --- a/state_machine/include/hand.h +++ b/state_machine/include/hand.h @@ -44,14 +44,16 @@ struct Hand { struct DealerHand : public Hand { constexpr DealerHand(const DealerHand& hand) = default; constexpr DealerHand(const Hand& hand) : Hand(hand) {} - explicit constexpr DealerHand(unsigned char v, bool soft = false, unsigned char cards = 1) + explicit constexpr DealerHand(unsigned char v, bool soft = false, + unsigned char cards = 1) : Hand(v, soft, cards) {} }; struct PlayerHand : public Hand { constexpr PlayerHand(const PlayerHand& hand) = default; constexpr PlayerHand(const Hand& hand) : Hand(hand) {} - explicit constexpr PlayerHand(unsigned char v, bool soft = false, unsigned char cards = 2) + explicit constexpr PlayerHand(unsigned char v, bool soft = false, + unsigned char cards = 2) : Hand(v, soft, cards) {} }; diff --git a/state_machine/include/state_machine.h b/state_machine/include/state_machine.h index 22e119d..6db1e2f 100644 --- a/state_machine/include/state_machine.h +++ b/state_machine/include/state_machine.h @@ -1,12 +1,12 @@ #ifndef STATE_MACHINE_H #define STATE_MACHINE_H +#include + #include "card.h" #include "ruleset.h" #include "state.h" -#include - namespace BlackjackEngine::StateMachine { // The result of a finished round, from the player's perspective. diff --git a/state_machine/src/state_machine.cpp b/state_machine/src/state_machine.cpp index 942342a..806c721 100644 --- a/state_machine/src/state_machine.cpp +++ b/state_machine/src/state_machine.cpp @@ -51,9 +51,9 @@ State EnterPlayer(const RuleSet& ruleset, const State& state) { actions |= Action::Double; } - // Re-enable Split only when the drawn card matches the original split card. - // For non-aces: hand score == splitCardScore * 2. - // For aces (splitCardScore == 11): hand is soft 12 (11 + 11 wraps). + // Re-enable Split only when the drawn card matches the original split + // card. For non-aces: hand score == splitCardScore * 2. For aces + // (splitCardScore == 11): hand is soft 12 (11 + 11 wraps). bool drawnCardMatchesSplit = state.splitCardScore > 0 && (state.playerHand.score == state.splitCardScore * 2 || @@ -110,7 +110,8 @@ Outcome Result(const State& state) { // Blackjacks (a two-card 21) are settled before comparing totals: two // blackjacks push, otherwise the side holding the blackjack wins. - const bool playerBlackjack = player.numCards == 2 && player.score == 21 && state.splitDepth == 0; + const bool playerBlackjack = + player.numCards == 2 && player.score == 21 && state.splitDepth == 0; const bool dealerBlackjack = dealer.numCards == 2 && dealer.score == 21; if (playerBlackjack || dealerBlackjack) { @@ -127,7 +128,7 @@ Outcome Result(const State& state) { } State InitializeState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, - DealerHand dealerHand, Action allowedActions) { + DealerHand dealerHand, Action allowedActions) { const State state(turn, playerHand, dealerHand, allowedActions); if (turn == Turn::Player) diff --git a/state_machine/tests/state_machine_tests.cpp b/state_machine/tests/state_machine_tests.cpp index 5ce0fd5..259ab69 100644 --- a/state_machine/tests/state_machine_tests.cpp +++ b/state_machine/tests/state_machine_tests.cpp @@ -17,58 +17,56 @@ State DealerTurn(PlayerHand player, DealerHand dealer) { } // namespace TEST(StateMachineTests, InitializeStateKeepsDoubleOnTwoCards) { - const State state = InitializeState( - RuleSet(), Turn::Player, PlayerHand(10), - DealerHand(6), Action::Hit | Action::Stand | Action::Double); + const State state = + InitializeState(RuleSet(), Turn::Player, PlayerHand(10), DealerHand(6), + Action::Hit | Action::Stand | Action::Double); EXPECT_EQ(state.turn, Turn::Player); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Double)); } TEST(StateMachineTests, InitializeStateRemovesDoubleWhenNotTwoCards) { const State state = InitializeState( - RuleSet(), Turn::Player, PlayerHand(15, false, 3), - DealerHand(6), Action::Hit | Action::Stand | Action::Double); + RuleSet(), Turn::Player, PlayerHand(15, false, 3), DealerHand(6), + Action::Hit | Action::Stand | Action::Double); EXPECT_FALSE(IsAllowed(state.allowedActions, Action::Double)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Stand)); } TEST(StateMachineTests, InitializeStateDealerEntersForcedHit) { - const State state = InitializeState( - RuleSet(), Turn::Dealer, PlayerHand(18), - DealerHand(9), Action::Hit | Action::Stand | Action::Double); + const State state = + InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), DealerHand(9), + Action::Hit | Action::Stand | Action::Double); EXPECT_EQ(state.turn, Turn::Dealer); EXPECT_EQ(state.allowedActions, Action::Hit); } TEST(StateMachineTests, InitializeStateDealerStandBecomesTerminal) { const State state = InitializeState( - RuleSet(), Turn::Dealer, PlayerHand(18), - DealerHand(19, false, 2), Action::Hit | Action::Stand | Action::Double); + RuleSet(), Turn::Dealer, PlayerHand(18), DealerHand(19, false, 2), + Action::Hit | Action::Stand | Action::Double); EXPECT_EQ(state.turn, Turn::End); EXPECT_EQ(state.allowedActions, Action::None); } TEST(StateMachineTests, StandMovesToDealer) { - const State next = Stand( - RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(9))); + const State next = + Stand(RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(9))); EXPECT_EQ(next.turn, Turn::Dealer); EXPECT_EQ(next.playerHand.score, 18); } TEST(StateMachineTests, HitStaysWithPlayerWhenNoBust) { - const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(12), DealerHand(9)), - Card(5)); + const State next = + Hit(RuleSet(), PlayerTurn(PlayerHand(12), DealerHand(9)), Card(5)); EXPECT_EQ(next.turn, Turn::Player); EXPECT_EQ(next.playerHand.score, 17); EXPECT_EQ(next.playerHand.numCards, 3); } TEST(StateMachineTests, HitBustEndsRound) { - const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(16), DealerHand(9)), - Card(10)); + const State next = + Hit(RuleSet(), PlayerTurn(PlayerHand(16), DealerHand(9)), Card(10)); EXPECT_EQ(next.turn, Turn::End); EXPECT_EQ(next.playerHand.score, 26); EXPECT_TRUE(IsTerminal(next)); @@ -76,8 +74,7 @@ TEST(StateMachineTests, HitBustEndsRound) { } TEST(StateMachineTests, DoubleAddsCardAndLeavesOnlyStand) { - const State next = Double( - PlayerTurn(PlayerHand(10), DealerHand(6)), Card(9)); + const State next = Double(PlayerTurn(PlayerHand(10), DealerHand(6)), Card(9)); EXPECT_EQ(next.turn, Turn::Player); EXPECT_EQ(next.playerHand.score, 19); EXPECT_EQ(next.playerHand.numCards, 3); @@ -87,48 +84,44 @@ TEST(StateMachineTests, DoubleAddsCardAndLeavesOnlyStand) { } TEST(StateMachineTests, DealerHitsBelow17) { - const State state = - InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), - DealerHand(16, false, 2)); + const State state = InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(16, false, 2)); EXPECT_FALSE(IsTerminal(state)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); } TEST(StateMachineTests, DealerStandsOnHard17) { - const State state = - InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), - DealerHand(17, false, 2)); + const State state = InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(17, false, 2)); EXPECT_TRUE(IsTerminal(state)); } TEST(StateMachineTests, DealerStandsAbove17) { - const State state = - InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), - DealerHand(19, false, 2)); + const State state = InitializeState(RuleSet(), Turn::Dealer, PlayerHand(18), + DealerHand(19, false, 2)); EXPECT_TRUE(IsTerminal(state)); } TEST(StateMachineTests, DealerStandsOnSoft17WhenRuleOff) { RuleSet ruleset; ruleset.hitOnSoft17 = false; - const State state = InitializeState( - ruleset, Turn::Dealer, PlayerHand(18), DealerHand(17, true, 2)); + const State state = InitializeState(ruleset, Turn::Dealer, PlayerHand(18), + DealerHand(17, true, 2)); EXPECT_TRUE(IsTerminal(state)); } TEST(StateMachineTests, DealerHitsOnSoft17WhenRuleOn) { RuleSet ruleset; ruleset.hitOnSoft17 = true; - const State state = InitializeState( - ruleset, Turn::Dealer, PlayerHand(18), DealerHand(17, true, 2)); + const State state = InitializeState(ruleset, Turn::Dealer, PlayerHand(18), + DealerHand(17, true, 2)); EXPECT_FALSE(IsTerminal(state)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); } TEST(StateMachineTests, HitAddsToDealerOnDealerTurn) { - const State next = Hit( - RuleSet(), DealerTurn(PlayerHand(18), DealerHand(10)), - Card(5)); + const State next = + Hit(RuleSet(), DealerTurn(PlayerHand(18), DealerHand(10)), Card(5)); EXPECT_EQ(next.dealerHand.score, 15); EXPECT_EQ(next.dealerHand.numCards, 2); EXPECT_EQ(next.playerHand.score, 18); @@ -136,15 +129,14 @@ TEST(StateMachineTests, HitAddsToDealerOnDealerTurn) { } TEST(StateMachineTests, StandEndsRoundOnDealerTurn) { - const State next = Stand(RuleSet(), DealerTurn(PlayerHand(18), - DealerHand(19, false, 2))); + const State next = + Stand(RuleSet(), DealerTurn(PlayerHand(18), DealerHand(19, false, 2))); EXPECT_EQ(next.turn, Turn::End); EXPECT_TRUE(IsTerminal(next)); } TEST(StateMachineTests, ResultDealerBustIsPlayerWin) { - const State state(Turn::End, PlayerHand(18), - DealerHand(23, false, 3)); + const State state(Turn::End, PlayerHand(18), DealerHand(23, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } @@ -155,14 +147,12 @@ TEST(StateMachineTests, ResultDealerBlackjackBeatsPlayer) { } TEST(StateMachineTests, ResultBothBlackjacksPush) { - const State state(Turn::End, PlayerHand(21), - DealerHand(21, false, 2)); + const State state(Turn::End, PlayerHand(21), DealerHand(21, false, 2)); EXPECT_EQ(Result(state), Outcome::Push); } TEST(StateMachineTests, ResultPlayerBlackjackBeatsNonBlackjack) { - const State state(Turn::End, PlayerHand(21), - DealerHand(21, false, 3)); + const State state(Turn::End, PlayerHand(21), DealerHand(21, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } @@ -191,39 +181,36 @@ TEST(StateMachineTests, ResultEqualIsPush) { } TEST(StateMachineTests, InitialPlayerStateAllowsAllActions) { - const State state = - PlayerTurn(PlayerHand(10), DealerHand(6)); + const State state = PlayerTurn(PlayerHand(10), DealerHand(6)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Hit)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Stand)); EXPECT_TRUE(IsAllowed(state.allowedActions, Action::Double)); } TEST(StateMachineTests, HitRemovesDoubleFromAllowedActions) { - const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(12), DealerHand(6)), - Card(3)); + const State next = + Hit(RuleSet(), PlayerTurn(PlayerHand(12), DealerHand(6)), Card(3)); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Stand)); EXPECT_FALSE(IsAllowed(next.allowedActions, Action::Double)); } TEST(StateMachineTests, BustLeavesNoAllowedActions) { - const State next = Hit( - RuleSet(), PlayerTurn(PlayerHand(16), DealerHand(6)), - Card(10)); + const State next = + Hit(RuleSet(), PlayerTurn(PlayerHand(16), DealerHand(6)), Card(10)); EXPECT_EQ(next.allowedActions, Action::None); } TEST(StateMachineTests, StandSetsDealerForcedHit) { - const State next = Stand( - RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(9))); + const State next = + Stand(RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(9))); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); EXPECT_FALSE(IsAllowed(next.allowedActions, Action::Stand)); } TEST(StateMachineTests, StandEndsRoundWhenDealerMustStand) { - const State next = Stand(RuleSet(), PlayerTurn(PlayerHand(18), - DealerHand(19, false, 2))); + const State next = + Stand(RuleSet(), PlayerTurn(PlayerHand(18), DealerHand(19, false, 2))); EXPECT_EQ(next.turn, Turn::End); EXPECT_TRUE(IsTerminal(next)); EXPECT_EQ(next.allowedActions, Action::None); @@ -232,14 +219,14 @@ TEST(StateMachineTests, StandEndsRoundWhenDealerMustStand) { TEST(StateMachineTests, DealerForcedActionRespectsHitOnSoft17) { RuleSet ruleset; ruleset.hitOnSoft17 = true; - const State next = Stand( - ruleset, PlayerTurn(PlayerHand(18), DealerHand(17, true, 2))); + const State next = + Stand(ruleset, PlayerTurn(PlayerHand(18), DealerHand(17, true, 2))); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); } TEST(StateMachineTests, StandAfterDoubleMovesToDealer) { - const State doubled = Double( - PlayerTurn(PlayerHand(10), DealerHand(9)), Card(9)); + const State doubled = + Double(PlayerTurn(PlayerHand(10), DealerHand(9)), Card(9)); const State next = Stand(RuleSet(), doubled); EXPECT_EQ(next.turn, Turn::Dealer); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit)); From 33c29406f6b4dbf9121a07d77ec0fdf9d94501f7 Mon Sep 17 00:00:00 2001 From: HenryLee Date: Tue, 4 Aug 2026 09:52:00 +1000 Subject: [PATCH 3/4] Include split in default allowed actions --- bayes/tests/bayes_macau_tests.cpp | 18 ++++++++++++------ state_machine/include/state.h | 2 +- state_machine/include/state_machine.h | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/bayes/tests/bayes_macau_tests.cpp b/bayes/tests/bayes_macau_tests.cpp index 3fad46a..66bcbf7 100644 --- a/bayes/tests/bayes_macau_tests.cpp +++ b/bayes/tests/bayes_macau_tests.cpp @@ -28,12 +28,6 @@ TEST(EvTests, TestPlayer16Dealer9Stand) { EXPECT_NEAR(ev.value, -0.543150, Epsilon); } -TEST(EvTests, TestPlayer16Dealer9Best) { - const ExpectedValue ev = - EvPlayerBestAction(macauRule, PlayerHand(16), DealerHand(9)); - EXPECT_NEAR(ev.value, -0.509322, Epsilon); -} - TEST(EvTests, TestPlayer16Dealer10Hit) { const ExpectedValue ev = EvPlayerHits(macauRule, PlayerHand(16), DealerHand(10)); @@ -154,6 +148,18 @@ TEST(EvTests, TestPlayer20Dealer6Split) { EXPECT_NEAR(ev.value, 0.575590, Epsilon); } +TEST(EvTests, TestPlayer16Dealer9Split) { + const ExpectedValue ev = + EvPlayerSplits(macauRule, PlayerHand(16), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.386573, Epsilon); +} + +TEST(EvTests, TestPlayer16Dealer9Best) { + const ExpectedValue ev = + EvPlayerBestAction(macauRule, PlayerHand(16), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.386573, Epsilon); +} + /* * Commented out because it is too slow at the moment. * We will introduce caches to fix this. diff --git a/state_machine/include/state.h b/state_machine/include/state.h index 5ddf847..e2d03ec 100644 --- a/state_machine/include/state.h +++ b/state_machine/include/state.h @@ -29,7 +29,7 @@ struct State { constexpr State(Turn turn, PlayerHand playerHand, DealerHand dealerHand, Action allowedActions = Action::Hit | Action::Stand | - Action::Double) + Action::Double | Action::Split) : turn(turn), playerHand(playerHand), dealerHand(dealerHand), diff --git a/state_machine/include/state_machine.h b/state_machine/include/state_machine.h index 6db1e2f..d0ec949 100644 --- a/state_machine/include/state_machine.h +++ b/state_machine/include/state_machine.h @@ -37,7 +37,7 @@ Outcome Result(const State& state); State InitializeState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, DealerHand dealerHand, Action allowedActions = Action::Hit | Action::Stand | - Action::Double); + Action::Double | Action::Split); // Stand: the acting party takes no more cards. On the player's turn play passes // to the dealer, who is advanced to their next forced state; on the dealer's From 0c068a8f2c47b65ff3e5ffa91d9363b7d8395804 Mon Sep 17 00:00:00 2001 From: HenryLee Date: Tue, 4 Aug 2026 16:07:05 +1000 Subject: [PATCH 4/4] Remove bayes/README.md --- bayes/README.md | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 bayes/README.md diff --git a/bayes/README.md b/bayes/README.md deleted file mode 100644 index 74218a7..0000000 --- a/bayes/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Blackjack Bayes - -This project is a library that uses Bayes' theorem to calculate the expected value of an action -based on a Blackjack state. - -# Build - -The following command builds the library and the unit test binary: - -```bash -cmake -B build -DCMAKE_BUILD_TYPE=Release -cmake --build build -``` - -It will generate the library: - -```bash -$ ls build/libblackjack_bayes.a -build/libblackjack_bayes.a -``` - -And the unit test binary: -```bash -$ ls build/tests/unit_tests -build/tests/unit_tests -```