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 -``` 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..e52d3d3 100644 --- a/bayes/src/bayes.cpp +++ b/bayes/src/bayes.cpp @@ -65,6 +65,13 @@ 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 +94,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)); + return EvStands( + ruleset, 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_macau_tests.cpp b/bayes/tests/bayes_macau_tests.cpp new file mode 100644 index 0000000..66bcbf7 --- /dev/null +++ b/bayes/tests/bayes_macau_tests.cpp @@ -0,0 +1,171 @@ +#include + +#include "bayes.h" + +using namespace BlackjackEngine::StateMachine; +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(macauRule, PlayerHand(16, false, 2), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.509322, Epsilon); +} + +TEST(EvTests, TestPlayer16Dealer9Stand) { + const ExpectedValue ev = + EvPlayerStands(macauRule, PlayerHand(16), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.543150, Epsilon); +} + +TEST(EvTests, TestPlayer16Dealer10Hit) { + const ExpectedValue ev = + EvPlayerHits(macauRule, PlayerHand(16), DealerHand(10)); + EXPECT_NEAR(ev.value, -0.575224, Epsilon); +} + +TEST(EvTests, TestPlayer16Dealer10Stand) { + const ExpectedValue ev = + EvPlayerStands(macauRule, PlayerHand(16), DealerHand(10)); + EXPECT_NEAR(ev.value, -0.575782, Epsilon); +} + +TEST(EvTests, TestPlayer16Dealer10Best) { + const ExpectedValue ev = + EvPlayerBestAction(macauRule, PlayerHand(16), DealerHand(10)); + EXPECT_NEAR(ev.value, -0.575224, Epsilon); +} + +TEST(EvTests, TestPlayer13Dealer6Hit) { + const ExpectedValue ev = + EvPlayerHits(macauRule, PlayerHand(13), DealerHand(6)); + EXPECT_NEAR(ev.value, -0.235626, Epsilon); +} + +TEST(EvTests, TestPlayer13Dealer6Stand) { + const ExpectedValue ev = + EvPlayerStands(macauRule, PlayerHand(13), DealerHand(6)); + EXPECT_NEAR(ev.value, -0.153699, Epsilon); +} + +TEST(EvTests, TestPlayer13Dealer6Best) { + const ExpectedValue ev = + EvPlayerBestAction(macauRule, PlayerHand(13), DealerHand(6)); + EXPECT_NEAR(ev.value, -0.153699, Epsilon); +} + +TEST(EvTests, TestPlayer10Dealer6Double) { + const ExpectedValue ev = + EvPlayerDoubles(macauRule, PlayerHand(10), DealerHand(6)); + EXPECT_NEAR(ev.value, 0.575590, Epsilon); +} + +TEST(EvTests, TestPlayer11Dealer7Double) { + const ExpectedValue ev = + EvPlayerDoubles(macauRule, PlayerHand(11), DealerHand(7)); + EXPECT_NEAR(ev.value, 0.462889, Epsilon); +} + +TEST(EvTests, TestPlayer9DealerAceHit) { + const ExpectedValue ev = + EvPlayerHits(macauRule, PlayerHand(9), DealerHand(11, true)); + EXPECT_NEAR(ev.value, -0.353164, Epsilon); +} + +TEST(EvTests, TestPlayer9DealerAceStand) { + const ExpectedValue ev = + EvPlayerStands(macauRule, PlayerHand(9), DealerHand(11, true)); + EXPECT_NEAR(ev.value, -0.769427, Epsilon); +} + +TEST(EvTests, TestPlayer4DealerAceHit) { + const ExpectedValue ev = + EvPlayerHits(macauRule, PlayerHand(4), DealerHand(11, true)); + EXPECT_NEAR(ev.value, -0.482899, Epsilon); +} + +TEST(EvTests, TestPlayer4DealerAceStand) { + const ExpectedValue ev = + EvPlayerStands(macauRule, PlayerHand(4), DealerHand(11, true)); + EXPECT_NEAR(ev.value, -0.769427, Epsilon); +} + +TEST(EvTests, TestPlayerSoft16Dealer9Hit) { + const ExpectedValue ev = + EvPlayerHits(macauRule, PlayerHand(16, true), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.148644, Epsilon); +} + +TEST(EvTests, TestPlayerSoft16Dealer9Stands) { + const ExpectedValue ev = + EvPlayerStands(macauRule, PlayerHand(16, true), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.543150, Epsilon); +} + +TEST(EvTests, TestPlayerSoft16Dealer9Doubles) { + const ExpectedValue ev = + EvPlayerDoubles(macauRule, PlayerHand(16, true), DealerHand(9)); + EXPECT_NEAR(ev.value, -0.456367, Epsilon); +} + +TEST(EvTests, TestPlayerSoft19DealerAceHits) { + const ExpectedValue ev = + EvPlayerHits(macauRule, PlayerHand(19, true), DealerHand(11, true)); + EXPECT_NEAR(ev.value, -0.311668, Epsilon); +} + +TEST(EvTests, TestPlayerSoft19DealerAceStands) { + const ExpectedValue ev = + 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); +} + +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. +TEST(EvTests, TestPlayerSoft12Dealer9Split) { + const ExpectedValue ev = + EvPlayerSplits(macauRule, PlayerHand(12, true), DealerHand(9)); + EXPECT_NEAR(ev.value, 0.642847, Epsilon); +} + */ diff --git a/bayes/tests/bayes_tests.cpp b/bayes/tests/bayes_tests.cpp deleted file mode 100644 index 91e89ec..0000000 --- a/bayes/tests/bayes_tests.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include - -#include "bayes.h" - -using namespace BlackjackEngine::StateMachine; -using namespace BlackjackEngine::Bayes; - -constexpr double Epsilon = 0.000001; - -TEST(EvTests, TestPlayer16Dealer9Hit) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), 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)); - EXPECT_NEAR(ev.value, -0.543150, Epsilon); -} - -TEST(EvTests, TestPlayer16Dealer9Best) { - const ExpectedValue ev = - EvPlayerBestAction(RuleSet(), PlayerHand(16, false, 2), DealerHand(9)); - EXPECT_NEAR(ev.value, -0.509322, Epsilon); -} - -TEST(EvTests, TestPlayer16Dealer10Hit) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(16, false, 2), DealerHand(10)); - EXPECT_NEAR(ev.value, -0.575224, Epsilon); -} - -TEST(EvTests, TestPlayer16Dealer10Stand) { - const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(16, false, 2), DealerHand(10)); - EXPECT_NEAR(ev.value, -0.575782, Epsilon); -} - -TEST(EvTests, TestPlayer16Dealer10Best) { - const ExpectedValue ev = - EvPlayerBestAction(RuleSet(), PlayerHand(16, false, 2), DealerHand(10)); - EXPECT_NEAR(ev.value, -0.575224, Epsilon); -} - -TEST(EvTests, TestPlayer13Dealer6Hit) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(13, false, 2), DealerHand(6)); - EXPECT_NEAR(ev.value, -0.235626, Epsilon); -} - -TEST(EvTests, TestPlayer13Dealer6Stand) { - const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(13, false, 2), DealerHand(6)); - EXPECT_NEAR(ev.value, -0.153699, Epsilon); -} - -TEST(EvTests, TestPlayer13Dealer6Best) { - const ExpectedValue ev = - EvPlayerBestAction(RuleSet(), PlayerHand(13, false, 2), DealerHand(6)); - EXPECT_NEAR(ev.value, -0.153699, Epsilon); -} - -TEST(EvTests, TestPlayer10Dealer6Double) { - const ExpectedValue ev = - EvPlayerDoubles(RuleSet(), PlayerHand(10, false, 2), DealerHand(6)); - EXPECT_NEAR(ev.value, 0.575590, Epsilon); -} - -TEST(EvTests, TestPlayer11Dealer7Double) { - const ExpectedValue ev = - EvPlayerDoubles(RuleSet(), PlayerHand(11, false, 2), DealerHand(7)); - EXPECT_NEAR(ev.value, 0.462889, Epsilon); -} - -TEST(EvTests, TestPlayer9DealerAceHit) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(9, false, 2), 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)); - EXPECT_NEAR(ev.value, -0.769427, Epsilon); -} - -TEST(EvTests, TestPlayer4DealerAceHit) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(4, false, 2), 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)); - EXPECT_NEAR(ev.value, -0.769427, Epsilon); -} - -TEST(EvTests, TestPlayerSoft16Dealer9Hit) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(16, true, 2), DealerHand(9)); - EXPECT_NEAR(ev.value, -0.148644, Epsilon); -} - -TEST(EvTests, TestPlayerSoft16Dealer9Stands) { - const ExpectedValue ev = - EvPlayerStands(RuleSet(), PlayerHand(16, true, 2), DealerHand(9)); - EXPECT_NEAR(ev.value, -0.543150, Epsilon); -} - -TEST(EvTests, TestPlayerSoft16Dealer9Doubles) { - const ExpectedValue ev = - EvPlayerDoubles(RuleSet(), PlayerHand(16, true, 2), DealerHand(9)); - EXPECT_NEAR(ev.value, -0.456367, Epsilon); -} - -TEST(EvTests, TestPlayerSoft19DealerAceHits) { - const ExpectedValue ev = - EvPlayerHits(RuleSet(), PlayerHand(19, true, 2), 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)); - EXPECT_NEAR(ev.value, -0.115483, 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..9d570a3 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,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) : 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..e2d03ec 100644 --- a/state_machine/include/state.h +++ b/state_machine/include/state.h @@ -19,9 +19,17 @@ 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) + 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 98a66a5..d0ec949 100644 --- a/state_machine/include/state_machine.h +++ b/state_machine/include/state_machine.h @@ -1,6 +1,8 @@ #ifndef STATE_MACHINE_H #define STATE_MACHINE_H +#include + #include "card.h" #include "ruleset.h" #include "state.h" @@ -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 | 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 @@ -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..806c721 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,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; + 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 +127,14 @@ Outcome Result(const State& state) { return Outcome::Push; } -State InitiateState(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); +State InitializeState(const RuleSet& ruleset, Turn turn, PlayerHand playerHand, + DealerHand dealerHand, Action 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 +158,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 +175,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..259ab69 100644 --- a/state_machine/tests/state_machine_tests.cpp +++ b/state_machine/tests/state_machine_tests.cpp @@ -16,59 +16,57 @@ 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( - RuleSet(), Turn::Player, PlayerHand(15, false, 3), - DealerHand(6, false, 1), Action::Hit | Action::Stand | Action::Double); +TEST(StateMachineTests, InitializeStateRemovesDoubleWhenNotTwoCards) { + const State state = InitializeState( + 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, 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), - DealerHand(19, false, 2), Action::Hit | Action::Stand | Action::Double); +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); } TEST(StateMachineTests, StandMovesToDealer) { - const State next = Stand( - RuleSet(), PlayerTurn(PlayerHand(18, false, 2), DealerHand(9, false, 1))); + 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, false, 2), DealerHand(9, false, 1)), - 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, false, 2), DealerHand(9, false, 1)), - 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, false, 2), DealerHand(6, false, 1)), 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 = - InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - 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 = - InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - 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 = - InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), - 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 = 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)), - 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, false, 2), - 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, false, 2), - 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, false, 2), - 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, false, 2), - 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, false, 2), DealerHand(6, false, 1)); + 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, false, 2), DealerHand(6, false, 1)), - 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, false, 2), DealerHand(6, false, 1)), - 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, false, 2), DealerHand(9, false, 1))); + 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, false, 2), - 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,15 +219,24 @@ TEST(StateMachineTests, StandEndsRoundWhenDealerMustStand) { TEST(StateMachineTests, DealerForcedActionRespectsHitOnSoft17) { RuleSet ruleset; ruleset.hitOnSoft17 = true; - const State next = Stand( - ruleset, PlayerTurn(PlayerHand(18, false, 2), 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, false, 2), DealerHand(9, false, 1)), 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)); } + +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); +}