From a674e090579398d958ee6a3b482b7a2b2e27f71f Mon Sep 17 00:00:00 2001 From: HenryLee Date: Mon, 27 Jul 2026 23:08:45 +1000 Subject: [PATCH] Add clang-format (Google style) with lint and lint-suggest workflows Adopt clang-format with the Google coding style and enforce it in CI. - Add a .clang-format based on the Google style and reformat the state machine and Bayes sources accordingly - Add a lint workflow that fails a push or pull request when any tracked C/C++ source is not clang-format clean - Add a lint-suggest workflow that posts the required formatting fixes as inline reviewdog suggestions on pull requests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60fdb396-8310-4c8b-b763-232614bf3a89 --- .clang-format | 1 + .github/workflows/lint-suggest.yml | 53 +++++++++ .github/workflows/lint.yml | 36 +++++++ bayes/include/bayes.h | 22 ++-- bayes/include/expected_value.h | 8 +- bayes/src/bayes.cpp | 68 ++++++------ bayes/tests/bayes_tests.cpp | 61 +++++++---- state_machine/include/action.h | 10 +- state_machine/include/card.h | 6 +- state_machine/include/hand.h | 34 +++--- state_machine/include/ruleset.h | 4 +- state_machine/include/state.h | 13 ++- state_machine/include/state_machine.h | 26 ++--- state_machine/src/state_machine.cpp | 40 +++---- state_machine/tests/hand_tests.cpp | 3 +- state_machine/tests/state_machine_tests.cpp | 113 +++++++++++++------- 16 files changed, 318 insertions(+), 180 deletions(-) create mode 100644 .clang-format create mode 100644 .github/workflows/lint-suggest.yml create mode 100644 .github/workflows/lint.yml diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f6cb8ad --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/.github/workflows/lint-suggest.yml b/.github/workflows/lint-suggest.yml new file mode 100644 index 0000000..d133885 --- /dev/null +++ b/.github/workflows/lint-suggest.yml @@ -0,0 +1,53 @@ +name: lint suggest + +# On pull requests, runs clang-format over the changed C/C++ files and posts the +# required formatting fixes as inline review suggestions via reviewdog. This is +# suggestion-only; the lint workflow is the blocking gate. + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + clang-format: + name: clang-format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install clang-format + run: pip install "clang-format==18.1.8" + + - name: Collect changed C/C++ files + id: changed + run: | + base="${{ github.event.pull_request.base.sha }}" + files=$(git diff --name-only --diff-filter=ACMR "$base" HEAD \ + | grep -E '\.(c|cc|cpp|cxx|h|hh|hpp)$' \ + | grep -E '^(bayes|state_machine)/' \ + | tr '\n' ' ' || true) + echo "files=$files" >> "$GITHUB_OUTPUT" + + - name: Run clang-format + if: steps.changed.outputs.files != '' + run: clang-format -i ${{ steps.changed.outputs.files }} + + - name: Suggest changes + if: steps.changed.outputs.files != '' + uses: reviewdog/action-suggester@v1 + with: + tool_name: clang-format + level: warning + fail_level: none + cleanup: "false" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8fd9b7e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: lint + +# Blocking clang-format check. Runs on every push and pull request and fails if +# any tracked C/C++ source in the project is not formatted according to +# .clang-format (Google style). The lint-suggest workflow posts inline fix +# suggestions on pull requests. + +on: + push: + pull_request: + +jobs: + clang-format: + name: clang-format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install clang-format + run: pip install "clang-format==18.1.8" + + - name: Check formatting + run: | + files=$(git ls-files '*.h' '*.cpp' '*.cc' '*.hpp' \ + | grep -E '^(bayes|state_machine)/' || true) + if [ -z "$files" ]; then + echo "No C/C++ files to check." + exit 0 + fi + clang-format --dry-run --Werror $files diff --git a/bayes/include/bayes.h b/bayes/include/bayes.h index 3d901fc..e766917 100644 --- a/bayes/include/bayes.h +++ b/bayes/include/bayes.h @@ -1,25 +1,25 @@ +#include + #include "expected_value.h" #include "hand.h" #include "ruleset.h" -#include - namespace BlackjackEngine::Bayes { using StateMachine::DealerHand; using StateMachine::PlayerHand; using StateMachine::RuleSet; -ExpectedValue EvPlayerBestAction(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand); +ExpectedValue EvPlayerBestAction(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand); -ExpectedValue EvPlayerStands(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand); +ExpectedValue EvPlayerStands(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand); -ExpectedValue EvPlayerHits(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand); +ExpectedValue EvPlayerHits(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand); -ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand); +ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand); -} // namespace BlackjackEngine::Bayes +} // namespace BlackjackEngine::Bayes diff --git a/bayes/include/expected_value.h b/bayes/include/expected_value.h index 683e912..423dc95 100644 --- a/bayes/include/expected_value.h +++ b/bayes/include/expected_value.h @@ -6,19 +6,19 @@ namespace BlackjackEngine::Bayes { struct Probability { double value = 0; - explicit constexpr Probability(double v) : value(v) { } + explicit constexpr Probability(double v) : value(v) {} }; struct ExpectedValue { double value = 0; - explicit constexpr ExpectedValue(double v) : value(v) { } + explicit constexpr ExpectedValue(double v) : value(v) {} bool operator<(ExpectedValue other) const { return this->value < other.value; } }; -} // namespace +} // namespace BlackjackEngine::Bayes -#endif // EXPECTED_VALUE_H +#endif // EXPECTED_VALUE_H diff --git a/bayes/src/bayes.cpp b/bayes/src/bayes.cpp index 3151421..22eb4c6 100644 --- a/bayes/src/bayes.cpp +++ b/bayes/src/bayes.cpp @@ -1,5 +1,5 @@ #include "bayes.h" -#include "state_machine.h" + #include #include #include @@ -7,22 +7,16 @@ #include #include +#include "state_machine.h" + using namespace BlackjackEngine::StateMachine; namespace BlackjackEngine::Bayes { constexpr std::array AllCards() { return { - Card(2), - Card(3), - Card(4), - Card(5), - Card(6), - Card(7), - Card(8), - Card(9), - Card(10), - Card(11), + Card(2), Card(3), Card(4), Card(5), Card(6), + Card(7), Card(8), Card(9), Card(10), Card(11), }; }; @@ -43,11 +37,13 @@ ExpectedValue EvBest(const RuleSet& ruleset, const State& state); // EV of hitting: draw a card, then play the resulting state optimally. ExpectedValue EvHits(const RuleSet& ruleset, const State& state) { auto allCards = std::views::all(AllCards()); - return std::accumulate(allCards.begin(), allCards.end(), ExpectedValue(0.0), - [&ruleset, state] (ExpectedValue current, Card card) { - return ExpectedValue(current.value + + return std::accumulate( + allCards.begin(), allCards.end(), ExpectedValue(0.0), + [&ruleset, state](ExpectedValue current, Card card) { + return ExpectedValue( + current.value + ProbOfGettingOneCard(card).value * - EvBest(ruleset, Hit(ruleset, state, card)).value); + EvBest(ruleset, Hit(ruleset, state, card)).value); }); } @@ -60,11 +56,12 @@ ExpectedValue EvStands(const RuleSet& ruleset, const State& state) { // resulting state optimally (only standing remains). ExpectedValue EvDoubles(const RuleSet& ruleset, const State& state) { auto allCards = std::views::all(AllCards()); - return std::accumulate(allCards.begin(), allCards.end(), ExpectedValue(0.0), - [&ruleset, state] (ExpectedValue current, Card card) { + return std::accumulate( + allCards.begin(), allCards.end(), ExpectedValue(0.0), + [&ruleset, state](ExpectedValue current, Card card) { return ExpectedValue(current.value + - 2.0 * ProbOfGettingOneCard(card).value * - EvBest(ruleset, Double(state, card)).value); + 2.0 * ProbOfGettingOneCard(card).value * + EvBest(ruleset, Double(state, card)).value); }); } @@ -73,8 +70,7 @@ ExpectedValue EvDoubles(const RuleSet& ruleset, const State& state) { // drives both the player's choices and the dealer's forced play, since a // dealer-turn state only ever allows Hit. ExpectedValue EvBest(const RuleSet& ruleset, const State& state) { - if (IsTerminal(state)) - return OutcomeValue(Result(state)); + if (IsTerminal(state)) return OutcomeValue(Result(state)); // A non-terminal state must offer at least one action, otherwise the fallback // below would return negative infinity. @@ -94,24 +90,28 @@ ExpectedValue EvBest(const RuleSet& ruleset, const State& state) { return best; } -ExpectedValue EvPlayerBestAction(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand) { - return EvBest(ruleset, InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); +ExpectedValue EvPlayerBestAction(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand) { + return EvBest(ruleset, + InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); } -ExpectedValue EvPlayerStands(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand) { - return EvStands(ruleset, InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); +ExpectedValue EvPlayerStands(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand) { + return EvStands(ruleset, + InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); } -ExpectedValue EvPlayerHits(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand) { - return EvHits(ruleset, InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); +ExpectedValue EvPlayerHits(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand) { + return EvHits(ruleset, + InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); } -ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, - PlayerHand playerHand, DealerHand dealerHand) { - return EvDoubles(ruleset, InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); +ExpectedValue EvPlayerDoubles(const RuleSet& ruleset, PlayerHand playerHand, + DealerHand dealerHand) { + return EvDoubles( + ruleset, InitiateState(ruleset, Turn::Player, playerHand, dealerHand)); } -} // namespace BlackjackEngine::Bayes +} // namespace BlackjackEngine::Bayes diff --git a/bayes/tests/bayes_tests.cpp b/bayes/tests/bayes_tests.cpp index d642914..91e89ec 100644 --- a/bayes/tests/bayes_tests.cpp +++ b/bayes/tests/bayes_tests.cpp @@ -1,4 +1,5 @@ #include + #include "bayes.h" using namespace BlackjackEngine::StateMachine; @@ -7,101 +8,121 @@ using namespace BlackjackEngine::Bayes; constexpr double Epsilon = 0.000001; TEST(EvTests, TestPlayer16Dealer9Hit) { - const ExpectedValue ev = EvPlayerHits(RuleSet(), PlayerHand(16, false, 2), DealerHand(9)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + const ExpectedValue ev = + EvPlayerStands(RuleSet(), PlayerHand(19, true, 2), DealerHand(11, true)); EXPECT_NEAR(ev.value, -0.115483, Epsilon); } diff --git a/state_machine/include/action.h b/state_machine/include/action.h index d6db272..6639efb 100644 --- a/state_machine/include/action.h +++ b/state_machine/include/action.h @@ -8,9 +8,9 @@ namespace BlackjackEngine::StateMachine { // the single action the ruleset forces (Hit or Stand); for a finished round it // is None. enum class Action : unsigned char { - None = 0, - Hit = 1u << 0, - Stand = 1u << 1, + None = 0, + Hit = 1u << 0, + Stand = 1u << 1, Double = 1u << 2, }; @@ -43,6 +43,6 @@ constexpr bool IsAllowed(Action set, Action action) { return (set & action) == action; } -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine -#endif // ACTION_H +#endif // ACTION_H diff --git a/state_machine/include/card.h b/state_machine/include/card.h index 6d586e9..89e3b94 100644 --- a/state_machine/include/card.h +++ b/state_machine/include/card.h @@ -6,9 +6,9 @@ namespace BlackjackEngine::StateMachine { struct Card { unsigned char score = 0; - explicit constexpr Card(unsigned char v) : score(v) { } + explicit constexpr Card(unsigned char v) : score(v) {} }; -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine -#endif // CARD_H +#endif // CARD_H diff --git a/state_machine/include/hand.h b/state_machine/include/hand.h index 217b636..b243083 100644 --- a/state_machine/include/hand.h +++ b/state_machine/include/hand.h @@ -10,20 +10,20 @@ struct Hand { bool isSoft = false; unsigned char numCards = 0; - explicit constexpr Hand(unsigned char v) : score(v), numCards(1) { } - constexpr Hand(unsigned char v, bool soft) : score(v), isSoft(soft), numCards(1) { } + explicit constexpr Hand(unsigned char v) : score(v), numCards(1) {} + constexpr Hand(unsigned char v, bool soft) + : score(v), isSoft(soft), numCards(1) {} constexpr Hand(unsigned char v, bool soft, unsigned char cards) - : score(v), isSoft(soft), numCards(cards) { } + : score(v), isSoft(soft), numCards(cards) {} constexpr Hand& operator+=(Card card) { - score = score + card.score; + score = score + card.score; ++numCards; if (score > 21) { if (card.score == 11) { score -= 10; - } - else if (isSoft) { + } else if (isSoft) { score -= 10; isSoft = false; } @@ -31,7 +31,7 @@ struct Hand { isSoft = true; } - return *this; + return *this; } constexpr Hand operator+(Card card) const { @@ -43,22 +43,22 @@ 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(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) - : Hand(v, soft, cards) { } + : 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(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) - : Hand(v, soft, cards) { } + : Hand(v, soft, cards) {} }; -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine -#endif // HAND_H +#endif // HAND_H diff --git a/state_machine/include/ruleset.h b/state_machine/include/ruleset.h index 81de97f..5d1a082 100644 --- a/state_machine/include/ruleset.h +++ b/state_machine/include/ruleset.h @@ -7,6 +7,6 @@ struct RuleSet { bool hitOnSoft17 = false; }; -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine -#endif // RULESET_H +#endif // RULESET_H diff --git a/state_machine/include/state.h b/state_machine/include/state.h index 866b4dc..1d74b3a 100644 --- a/state_machine/include/state.h +++ b/state_machine/include/state.h @@ -20,11 +20,14 @@ struct State { Action allowedActions; constexpr State(Turn turn, PlayerHand playerHand, DealerHand dealerHand, - Action allowedActions = Action::Hit | Action::Stand | Action::Double) - : turn(turn), playerHand(playerHand), dealerHand(dealerHand), - allowedActions(allowedActions) { } + Action allowedActions = Action::Hit | Action::Stand | + Action::Double) + : turn(turn), + playerHand(playerHand), + dealerHand(dealerHand), + allowedActions(allowedActions) {} }; -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine -#endif // STATE_H +#endif // STATE_H diff --git a/state_machine/include/state_machine.h b/state_machine/include/state_machine.h index e2c89a1..98a66a5 100644 --- a/state_machine/include/state_machine.h +++ b/state_machine/include/state_machine.h @@ -28,22 +28,24 @@ Outcome Result(const State& state); // Transition functions: produce the next state from the current one. // --------------------------------------------------------------------------- -// Build a starting state. Double is stripped from the allowed actions unless 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); +// Build a starting state. Double is stripped from the allowed actions unless +// 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); // 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 // turn the round ends with no further actions. State Stand(const RuleSet& ruleset, const State& state); -// A card is drawn for whoever is acting. On the player's turn it is added to the -// player hand; the hand may no longer double, and a bust ends the round. On the -// dealer's turn it is added to the dealer hand and the dealer is advanced to -// their next forced state. +// A card is drawn for whoever is acting. On the player's turn it is added to +// the player hand; the hand may no longer double, and a bust ends the round. On +// the dealer's turn it is added to the dealer hand and the dealer is advanced +// to their next forced state. State Hit(const RuleSet& ruleset, const State& state, Card card); // Double: a single card is drawn, after which the acting party may only stand; @@ -51,6 +53,6 @@ State Hit(const RuleSet& ruleset, const State& state, Card card); // allowed to double. State Double(const State& state, Card card); -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine -#endif // STATE_MACHINE_H +#endif // STATE_MACHINE_H diff --git a/state_machine/src/state_machine.cpp b/state_machine/src/state_machine.cpp index 34a2c86..ddd47dd 100644 --- a/state_machine/src/state_machine.cpp +++ b/state_machine/src/state_machine.cpp @@ -10,11 +10,9 @@ namespace { bool DealerShouldHit(const RuleSet& ruleset, const State& state) { const DealerHand& hand = state.dealerHand; - if (hand.score < 17) - return true; + if (hand.score < 17) return true; - if (hand.score > 17) - return false; + if (hand.score > 17) return false; return hand.isSoft && ruleset.hitOnSoft17; } @@ -35,21 +33,17 @@ State EnterDealer(const RuleSet& ruleset, State state) { return state; } -} // namespace +} // namespace -bool IsTerminal(const State& state) { - return state.turn == Turn::End; -} +bool IsTerminal(const State& state) { return state.turn == Turn::End; } Outcome Result(const State& state) { const PlayerHand& player = state.playerHand; const DealerHand& dealer = state.dealerHand; - if (player.score > 21) - return Outcome::PlayerLoses; + if (player.score > 21) return Outcome::PlayerLoses; - if (dealer.score > 21) - return Outcome::PlayerWins; + if (dealer.score > 21) return Outcome::PlayerWins; // Blackjacks (a two-card 21) are settled before comparing totals: two // blackjacks push, otherwise the side holding the blackjack wins. @@ -57,31 +51,26 @@ Outcome Result(const State& state) { const bool dealerBlackjack = dealer.numCards == 2 && dealer.score == 21; if (playerBlackjack || dealerBlackjack) { - if (playerBlackjack && dealerBlackjack) - return Outcome::Push; + if (playerBlackjack && dealerBlackjack) return Outcome::Push; return playerBlackjack ? Outcome::PlayerWins : Outcome::PlayerLoses; } - if (dealer.score > player.score) - return Outcome::PlayerLoses; + if (dealer.score > player.score) return Outcome::PlayerLoses; - if (dealer.score < player.score) - return Outcome::PlayerWins; + if (dealer.score < player.score) return Outcome::PlayerWins; return Outcome::Push; } -State InitiateState(const RuleSet& ruleset, Turn turn, - PlayerHand playerHand, DealerHand dealerHand, - Action allowedActions) { +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); - if (turn == Turn::Dealer) - return EnterDealer(ruleset, state); + if (turn == Turn::Dealer) return EnterDealer(ruleset, state); return state; } @@ -89,8 +78,7 @@ State InitiateState(const RuleSet& ruleset, Turn turn, State Stand(const RuleSet& ruleset, const State& state) { State next = state; - if (state.turn == Turn::Player) - return EnterDealer(ruleset, next); + if (state.turn == Turn::Player) return EnterDealer(ruleset, next); next.turn = Turn::End; next.allowedActions = Action::None; @@ -128,4 +116,4 @@ State Double(const State& state, Card card) { return next; } -} // namespace BlackjackEngine::StateMachine +} // namespace BlackjackEngine::StateMachine diff --git a/state_machine/tests/hand_tests.cpp b/state_machine/tests/hand_tests.cpp index 13f1a67..282e358 100644 --- a/state_machine/tests/hand_tests.cpp +++ b/state_machine/tests/hand_tests.cpp @@ -1,4 +1,5 @@ #include + #include "hand.h" using namespace BlackjackEngine::StateMachine; @@ -129,12 +130,10 @@ TEST(HandTests, TestSoftHandAddCard) { EXPECT_EQ(hand.isSoft, true); } - { Hand hand(20, true); hand += Card(11); EXPECT_EQ(hand.score, 21); EXPECT_EQ(hand.isSoft, true); } - } diff --git a/state_machine/tests/state_machine_tests.cpp b/state_machine/tests/state_machine_tests.cpp index d89a14c..a7857dc 100644 --- a/state_machine/tests/state_machine_tests.cpp +++ b/state_machine/tests/state_machine_tests.cpp @@ -1,4 +1,5 @@ #include + #include "state_machine.h" using namespace BlackjackEngine::StateMachine; @@ -13,56 +14,61 @@ State DealerTurn(PlayerHand player, DealerHand dealer) { return State(Turn::Dealer, player, dealer); } -} // namespace +} // 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); + const State state = InitiateState( + RuleSet(), Turn::Player, PlayerHand(10, false, 2), + DealerHand(6, false, 1), 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); + const State state = InitiateState( + RuleSet(), Turn::Player, PlayerHand(15, false, 3), + DealerHand(6, false, 1), 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); + const State state = InitiateState( + RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), + DealerHand(9, false, 1), 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); + const State state = InitiateState( + RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), + 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, false, 2), DealerHand(9, false, 1))); 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, false, 2), DealerHand(9, false, 1)), + 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, false, 2), DealerHand(9, false, 1)), + Card(10)); EXPECT_EQ(next.turn, Turn::End); EXPECT_EQ(next.playerHand.score, 26); EXPECT_TRUE(IsTerminal(next)); @@ -70,7 +76,8 @@ 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, false, 2), DealerHand(6, false, 1)), Card(9)); EXPECT_EQ(next.turn, Turn::Player); EXPECT_EQ(next.playerHand.score, 19); EXPECT_EQ(next.playerHand.numCards, 3); @@ -80,38 +87,48 @@ TEST(StateMachineTests, DoubleAddsCardAndLeavesOnlyStand) { } TEST(StateMachineTests, DealerHitsBelow17) { - const State state = InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), DealerHand(16, false, 2)); + const State state = + InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), + 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 = + InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), + 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 = + InitiateState(RuleSet(), Turn::Dealer, PlayerHand(18, false, 2), + 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 = InitiateState( + ruleset, Turn::Dealer, PlayerHand(18, false, 2), 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 = InitiateState( + ruleset, Turn::Dealer, PlayerHand(18, false, 2), 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, false, 2), DealerHand(10, false, 1)), + Card(5)); EXPECT_EQ(next.dealerHand.score, 15); EXPECT_EQ(next.dealerHand.numCards, 2); EXPECT_EQ(next.playerHand.score, 18); @@ -119,78 +136,94 @@ 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, false, 2), + 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, false, 2), + DealerHand(23, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } TEST(StateMachineTests, ResultDealerBlackjackBeatsPlayer) { - const State state(Turn::End, PlayerHand(20, false, 3), DealerHand(21, false, 2)); + const State state(Turn::End, PlayerHand(20, false, 3), + DealerHand(21, false, 2)); EXPECT_EQ(Result(state), Outcome::PlayerLoses); } TEST(StateMachineTests, ResultBothBlackjacksPush) { - const State state(Turn::End, PlayerHand(21, false, 2), DealerHand(21, false, 2)); + const State state(Turn::End, PlayerHand(21, false, 2), + 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, false, 2), + DealerHand(21, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } TEST(StateMachineTests, ResultThreeCard21IsNotBlackjack) { - const State state(Turn::End, PlayerHand(21, false, 3), DealerHand(21, false, 3)); + const State state(Turn::End, PlayerHand(21, false, 3), + DealerHand(21, false, 3)); EXPECT_EQ(Result(state), Outcome::Push); } TEST(StateMachineTests, ResultHigherPlayerWins) { - const State state(Turn::End, PlayerHand(20, false, 3), DealerHand(18, false, 3)); + const State state(Turn::End, PlayerHand(20, false, 3), + DealerHand(18, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerWins); } TEST(StateMachineTests, ResultLowerPlayerLoses) { - const State state(Turn::End, PlayerHand(17, false, 3), DealerHand(19, false, 3)); + const State state(Turn::End, PlayerHand(17, false, 3), + DealerHand(19, false, 3)); EXPECT_EQ(Result(state), Outcome::PlayerLoses); } TEST(StateMachineTests, ResultEqualIsPush) { - const State state(Turn::End, PlayerHand(19, false, 3), DealerHand(19, false, 3)); + const State state(Turn::End, PlayerHand(19, false, 3), + DealerHand(19, false, 3)); EXPECT_EQ(Result(state), Outcome::Push); } TEST(StateMachineTests, InitialPlayerStateAllowsAllActions) { - const State state = PlayerTurn(PlayerHand(10, false, 2), DealerHand(6, false, 1)); + const State state = + PlayerTurn(PlayerHand(10, false, 2), DealerHand(6, false, 1)); 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, false, 2), DealerHand(6, false, 1)), + 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, false, 2), DealerHand(6, false, 1)), + 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, false, 2), DealerHand(9, false, 1))); 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, false, 2), + DealerHand(19, false, 2))); EXPECT_EQ(next.turn, Turn::End); EXPECT_TRUE(IsTerminal(next)); EXPECT_EQ(next.allowedActions, Action::None); @@ -199,12 +232,14 @@ 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, false, 2), 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, false, 2), DealerHand(9, false, 1)), Card(9)); const State next = Stand(RuleSet(), doubled); EXPECT_EQ(next.turn, Turn::Dealer); EXPECT_TRUE(IsAllowed(next.allowedActions, Action::Hit));