diff --git a/.github/workflows/black-lint.yml b/.github/workflows/black-lint.yml index 311edbe..21c7f51 100644 --- a/.github/workflows/black-lint.yml +++ b/.github/workflows/black-lint.yml @@ -3,12 +3,8 @@ name: Black Lint on: push: branches: [main, dev] - paths: - - '**/*.py' pull_request: branches: [main] - paths: - - '**/*.py' workflow_dispatch: concurrency: diff --git a/.gitignore b/.gitignore index 89304cb..e96a541 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .claude/ +CLAUDE.md + *.pyc __pychache__/ .pytest_cache/ diff --git a/PWA.md b/PWA.md new file mode 100644 index 0000000..1fbbcaa --- /dev/null +++ b/PWA.md @@ -0,0 +1,72 @@ +# ๐Ÿ“ฒInstall MonkeyJOB App + +Let MonkeyJob be part of your desktop: + +--- + +## ๐Ÿ“ฑiOS + +1. Open [www.jack-s-onlineblackjack.com](https://www.jack-s-onlineblackjack.com) in Safari. +2. Tap the **Share** button โ€” square with an arrow one. +3. Scroll down and tap **Add to Home Screen**. +4. Confirm โ€” icon lands on your home screen, like any other app. + +--- + +## ๐Ÿ’ปmacOS + +**๐ŸงญSafari** +1. Open [www.jack-s-onlineblackjack.com](https://www.jack-s-onlineblackjack.com). + +2. Menu bar โ†’ **File โ†’ Add to Dock**. + + โ†ณ Or share icon โ†’ Add to Dock. + +3. MonkeyJob appears in your Dock as a standalone app. + +**๐ŸŒˆChrome** +1. Open [www.jack-s-onlineblackjack.com](https://www.jack-s-onlineblackjack.com). + +2. Click **Install** icon on address bar's right side. + + โ†ณ Or โ‹ฎ menu โ†’ Cast, Save, and Share โ†’ Install MonkeyJob. + +3. Find it in **Launchpad**, or drag it into your Dock. + +--- + +## ๐Ÿค–Android + +1. Open [www.jack-s-onlineblackjack.com](https://www.jack-s-onlineblackjack.com) in Chrome. + +2. Tap the **โ‹ฎ** menu โ†’ **Install app**. + + โ†ณ Tap **Install** if Chrome shows a banner automatically. + +3. Confirm โ€” MonkeyJob is added to your home screen and app drawer. + +--- + +## ๐ŸชŸWindows + +**Edge** โ€” best support +1. Open [www.jack-s-onlineblackjack.com](https://www.jack-s-onlineblackjack.com). + +2. Click **Install** icon on address bar's right side. + + โ†ณ Or โ‹ฏ menu โ†’ Apps โ†’ Install this site as an app. + +3. MonkeyJob opens in its own window and is added to Start Menu. + +**๐ŸŒˆChrome** +1. Open [www.jack-s-onlineblackjack.com](https://www.jack-s-onlineblackjack.com). + +2. Click **Install** icon on address bar's right side. + + โ†ณ Or โ‹ฎ menu โ†’ Cast, Save, and Share โ†’ Install MonkeyJob. + +3. Find it in Start Menu, or pin it to your taskbar. + +--- + +JOB โ€” **J**ack's **O**nline **B**lackjack โ€” is officially in your desktop ๐ŸŽƒ diff --git a/README.md b/README.md index 3b55098..8669a90 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,23 @@ # ๐ŸŽฐMonkeyJOB: Jack's Online Blackjack +[![Installation Guide](https://img.shields.io/badge/๐Ÿ“ฒ%20Installation-Guide%20๐Ÿ“–-0d1f17?style=for-the-badge)](./PWA.md) + +[![iOS](https://img.shields.io/badge/iOS-000000?style=for-the-badge&logo=apple&logoColor=white)](https://www.jack-s-onlineblackjack.com) +[![Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.jack-s-onlineblackjack.com) + +[![macOS](https://img.shields.io/badge/macOS-A2AAAD?style=for-the-badge&logo=apple&logoColor=black)](https://www.jack-s-onlineblackjack.com) +[![Windows](https://img.shields.io/badge/Windows-0078D4?style=for-the-badge&logo=windows&logoColor=white)](https://www.jack-s-onlineblackjack.com) + +### ๐Ÿ›ฐ๏ธ Metadata + [![main Unit Tests](https://github.com/StarsExpress/MonkeyJOB/actions/workflows/unit_tests.yml/badge.svg?branch=main)](https://github.com/StarsExpress/MonkeyJOB/actions) [![main Component Tests](https://github.com/StarsExpress/MonkeyJOB/actions/workflows/component_tests.yml/badge.svg?branch=main)](https://github.com/StarsExpress/MonkeyJOB/actions) [![main Integration Tests](https://github.com/StarsExpress/MonkeyJOB/actions/workflows/integration_tests.yml/badge.svg?branch=main)](https://github.com/StarsExpress/MonkeyJOB/actions) -[![main Black Linter](https://github.com/StarsExpress/MonkeyJOB/actions/workflows/black-lint.yml/badge.svg?branch=main)](https://github.com/StarsExpress/MonkeyJOB/actions) +[![main Black Lint](https://github.com/StarsExpress/MonkeyJOB/actions/workflows/black-lint.yml/badge.svg?branch=main)](https://github.com/StarsExpress/MonkeyJOB/actions) +[![Latest Release](https://img.shields.io/github/v/release/StarsExpress/MonkeyJOB)](https://github.com/StarsExpress/MonkeyJOB/releases) + +--- ### [๐Ÿคต๐Ÿป Let's Have A Seat ๐Ÿต](https://www.jack-s-onlineblackjack.com) diff --git a/blackjack.py b/blackjack.py index 7211511..7c4d8ec 100644 --- a/blackjack.py +++ b/blackjack.py @@ -3,7 +3,13 @@ from utils.judges import judge_blackjack, judge_surrender, judge_split from utils.trackers import update_properties, track_display_value from configs.hands_config import MIN_DEALER_VALUE, MAX_TOTAL_VALUE, HANDS_RANGE -from configs.bets_config import MAX_CAPITAL, MIN_BET, MAX_BET, BLACKJACK_PAY +from configs.bets_config import ( + MAX_CAPITAL, + MIN_BET, + MAX_BET, + BLACKJACK_PAY, + HUGE_PROFIT_RATIO, +) from configs.display_config import DEFAULT_PLAYER_NAME, DANGER_ZONE, MAX_NAME_LEN @@ -38,6 +44,10 @@ def __init__(self): self._initial_capital = 0 self._profits: dict = dict() + # Whole-round ROI congrats payload, set once at settlement when the + # round clears HUGE_PROFIT_RATIO; None otherwise. See _close_round. + self._congrats: dict | None = None + # Keys: hand_branch. # Values categories: # I. Blackjack related: bj | early_pay. @@ -96,6 +106,7 @@ def start_round(self, bets: list) -> dict: self._profits.clear() # Reset for new round. self._outcomes.clear() + self._congrats = None self._bj_early_pay_queue.clear() # Reset for new round. self._early_paid_hands.clear() @@ -491,12 +502,36 @@ def _close_round(self): } ) + self._flag_huge_profit(total_profit) + self.round_num += 1 self._phase = "settled" self._active_hand = None self._active_branch = None + def _flag_huge_profit(self, net_profit: int): + """Whole-round ROI check (ported from v1.0.0 POPUP_DICT["huge_profits"]). + + Runs once per round, only here at full settlement. The denominator is + every chip committed this round: base bets, double/split added wagers + (captured by each branch's chips), and insurance. If net profit over + that total clears HUGE_PROFIT_RATIO, flag a one-time congrats payload + for the final settlement response to carry; the frontend renders it. + """ + committed = 0 + for hand in self.player.hands_dict.values(): + for branch_id in hand.cards_dict: + committed += hand.chips_dict.get(branch_id, hand.initial_chips) + committed += hand.insurance + + if committed <= 0: + return + + ratio = net_profit / committed + if ratio >= HUGE_PROFIT_RATIO: + self._congrats = {"profit_rate": round(ratio * 100, 1)} + def _serialize(self) -> dict: dealer_data = { "cards": [ @@ -620,6 +655,9 @@ def _serialize(self) -> dict: "round": self.round_num, } + if self._phase == "settled" and self._congrats is not None: + result["congrats"] = self._congrats + if self._phase == "playing" and self._active_hand: result["active_hand"] = self._active_hand result["active_branch"] = self._active_branch diff --git a/configs/bets_config.py b/configs/bets_config.py index bcca43d..f766368 100644 --- a/configs/bets_config.py +++ b/configs/bets_config.py @@ -6,3 +6,9 @@ MIN_BET = 300 BLACKJACK_PAY = 1.5 + +# Whole-round ROI congrats popup (ported from v1.0.0 POPUP_DICT["huge_profits"]). +# Fires once after a round is fully settled, when the round's net profit divided +# by the total chips committed (base bets + double/split wagers + insurance) +# meets this ratio. 0.7 == a 70% return on everything staked that round. +HUGE_PROFIT_RATIO = 0.7 diff --git a/static/app.js b/static/app.js index 94ec1e5..5ef64eb 100644 --- a/static/app.js +++ b/static/app.js @@ -6,6 +6,14 @@ const MAX_NAME_LEN = 50; const SUIT_SYMBOL = { S: 'โ™ ', H: 'โ™ฅ', D: 'โ™ฆ', C: 'โ™ฃ' }; const SUIT_COLOR = { S: 'black', H: 'red', D: 'red', C: 'black' }; +// Whole-round "huge profit" congrats popup (ported from v1.0.0). The server +// decides whether it fires (sends `congrats.profit_rate`); these are the +// display strings only. +const HUGE_PROFIT_POPUP = { + title: '๐ŸŽ‰ Huge Profit! ๐ŸŽ‰', + body: rate => `๐Ÿ’ต ${rate}% Profit Rate ๐Ÿพ`, +}; + // โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const state = { balance: 0, @@ -86,6 +94,7 @@ function enterBettingPhase() { updateBetDisplay(); renderBalance(state.balance); // bets reset to 0; restore full balance clearTable(); + hideHugeProfit(); } // Build the 1โ€“6 count buttons @@ -284,7 +293,12 @@ async function updateUI(data) { renderHands(data.hands, data.phase); showPhase('settled'); flashRoundResult(data.hands); - if (data.capital < MIN_BET) setTimeout(showGameOver, 2500); + if (data.capital < MIN_BET) { + setTimeout(showGameOver, 2500); + } else if (data.congrats) { + // One celebratory popup, after the dealer reveal + result flash. + setTimeout(() => showHugeProfit(data.congrats.profit_rate), 1800); + } } else { renderDealer(data.dealer); renderHands(data.hands, data.phase); @@ -641,6 +655,17 @@ function showGameOver() { document.getElementById('gameover-modal').style.display = 'flex'; } +// โ”€โ”€ Huge-profit congrats modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function showHugeProfit(profitRate) { + document.getElementById('huge-profit-body').textContent = + HUGE_PROFIT_POPUP.body(profitRate); + document.getElementById('huge-profit-modal').style.display = 'flex'; +} + +function hideHugeProfit() { + document.getElementById('huge-profit-modal').style.display = 'none'; +} + // โ”€โ”€ GitHub modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function showGitHub() { document.getElementById('github-modal').style.display = 'flex'; diff --git a/static/index.html b/static/index.html index 6d9a06b..7e2d407 100644 --- a/static/index.html +++ b/static/index.html @@ -6,10 +6,19 @@ ๐ŸŽฐMonkeyJOB's Here - + + + + +
@@ -32,7 +41,7 @@

โ™ ๏ธโ™ฅ๏ธMonkeyJOBโ™ฆ๏ธโ™ฃ๏ธ
Jack's Online Blackjack

-
โ™ ๏ธโ™ฅ๏ธJOB: Jack's Online Blackjackโ™ฆ๏ธโ™ฃ๏ธ
+
โ™ ๏ธโ™ฅ๏ธJOB: Jack's Online Blackjackโ™ฆ๏ธโ™ฃ๏ธ
Balance  $0
+ + + - + diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..54816c9 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "MonkeyJOB", + "short_name": "MonkeyJOB", + "description": "Jack's Online Blackjack โ€” a Macau-rules Blackjack game.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "landscape", + "background_color": "#0d1f14", + "theme_color": "#0d1f14", + "icons": [ + { + "src": "/static/images/logo.png", + "sizes": "1024x1024", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/static/style.css b/static/style.css index 97dd579..9d9d8b0 100644 --- a/static/style.css +++ b/static/style.css @@ -31,6 +31,41 @@ body { user-select: none; } +/* โ”€โ”€ ROTATE PROMPT (landscape-only) โ”€โ”€ */ +.rotate-overlay { + display: none; + position: fixed; + inset: 0; + background: #0d1f14; + z-index: 10000; + align-items: center; + justify-content: center; + padding: 16px; + text-align: center; +} + +.rotate-msg p { + margin-top: 16px; + font-size: 1.05rem; + color: var(--ivory); +} + +.rotate-icon { + display: inline-block; + font-size: 3rem; + animation: rotate-hint 2s ease-in-out infinite; +} + +@keyframes rotate-hint { + 0%, 25% { transform: rotate(0deg); } + 50%, 100% { transform: rotate(90deg); } +} + +/* Phones and tablets held in portrait. Desktop windows are wide, so they are unaffected. */ +@media (orientation: portrait) and (max-width: 1024px) { + .rotate-overlay { display: flex; } +} + /* โ”€โ”€ SESSION OVERLAY โ”€โ”€ */ .session-overlay { position: fixed; @@ -979,3 +1014,75 @@ body { font-size: 0.85rem; letter-spacing: 0.05em; } + +/* โ”€โ”€ HUGE PROFIT MODAL (whole-round ROI congrats) โ”€โ”€ */ +.hugeprofit-modal { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + width: 100%; height: 100%; + background: rgba(0,0,0,0.82); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.hugeprofit-card { + background: linear-gradient(160deg, #1a3a24, #0d1f14); + border: 1px solid var(--gold); + border-radius: 14px; + padding: 36px 44px 40px; + width: min(420px, 92vw); + display: flex; + flex-direction: column; + align-items: center; + gap: 18px; + box-shadow: 0 24px 60px rgba(0,0,0,0.7); + text-align: center; +} +.hugeprofit-title { + font-family: 'Playfair Display', serif; + font-size: 1.6rem; + color: var(--gold); + letter-spacing: 0.05em; + margin: 0; +} +.hugeprofit-body { + font-family: 'JetBrains Mono', monospace; + font-size: 1.15rem; + color: var(--ivory); + letter-spacing: 0.03em; +} +.hugeprofit-ok-btn { + padding: 10px 44px; + font-size: 0.9rem; + letter-spacing: 0.06em; + margin-top: 6px; +} + +/* โ”€โ”€ SHORT LANDSCAPE (phones held in landscape) โ”€โ”€ + Here the limiting dimension is HEIGHT, not width: a full-size header + would eat the vertical space the dealer + 6-hand row need. Collapse the + top bar into one slim row and trim vertical padding on the table zones. + Desktop, tablet, and portrait layouts are untouched. */ +@media (orientation: landscape) and (max-height: 480px) { + /* Top bar โ†’ one tight single row */ + .topbar { padding: 4px 14px; } + .topbar-title { + font-size: 0.95rem; + letter-spacing: 0.03em; + white-space: nowrap; + } + /* Drop the decorative suits and the long subtitle; keep only "JOB" */ + .topbar-title .tt-deco, + .topbar-title .tt-full { display: none; } + .topbar-balance { font-size: 0.72rem; } + .topbar-balance span { font-size: 0.82rem; } + .topbar-nav { gap: 6px; } + .nav-btn { font-size: 0.62rem; padding: 3px 9px; } + + /* Reclaim vertical room so a 6-hand deal fits without scrolling */ + .dealer-zone { padding: 6px 0 4px; } + .dealer-zone .zone-label { margin-bottom: 4px; } + .dealer-value-badge { margin-top: 5px; } + .player-zone { padding-top: 4px; } +} diff --git a/tests/component/hand_processor_test.py b/tests/component/hand_processor_test.py index 35af955..0effe2a 100644 --- a/tests/component/hand_processor_test.py +++ b/tests/component/hand_processor_test.py @@ -6,8 +6,8 @@ class TestInitialization(unittest.TestCase): """Test HandProcessor.__init__ initializes state correctly.""" def test_hand_states(self): - hand_processor = HandProcessor('1', 500, ['7', '8'], ['S', 'H']) - self.assertEqual(hand_processor.head_ordinal, '1') + hand_processor = HandProcessor("1", 500, ["7", "8"], ["S", "H"]) + self.assertEqual(hand_processor.head_ordinal, "1") self.assertEqual(hand_processor.initial_chips, 500) self.assertEqual(hand_processor.splits, 0) self.assertEqual(hand_processor.insurance, 0) @@ -16,43 +16,43 @@ def test_hand_states(self): self.assertFalse(hand_processor.aces_pair) def test_hand_dicts(self): - hand_processor = HandProcessor('1', 500, ['7', '8'], ['S', 'H']) - self.assertEqual(hand_processor.chips_dict, {'1': 500}) - self.assertEqual(hand_processor.double_down_dict, {'1': False}) - self.assertEqual(hand_processor.cards_dict, {'1': ['7', '8']}) - self.assertEqual(hand_processor.suits_dict, {'1': ['S', 'H']}) + hand_processor = HandProcessor("1", 500, ["7", "8"], ["S", "H"]) + self.assertEqual(hand_processor.chips_dict, {"1": 500}) + self.assertEqual(hand_processor.double_down_dict, {"1": False}) + self.assertEqual(hand_processor.cards_dict, {"1": ["7", "8"]}) + self.assertEqual(hand_processor.suits_dict, {"1": ["S", "H"]}) def test_hand_properties(self): - hand_processor = HandProcessor('1', 500, ['7', '8'], ['S', 'H']) - self.assertEqual(hand_processor.value_dict['1'], 15) - self.assertFalse(hand_processor.soft_dict['1']) - self.assertFalse(hand_processor.bust_dict['1']) + hand_processor = HandProcessor("1", 500, ["7", "8"], ["S", "H"]) + self.assertEqual(hand_processor.value_dict["1"], 15) + self.assertFalse(hand_processor.soft_dict["1"]) + self.assertFalse(hand_processor.bust_dict["1"]) def test_blackjack_detection(self): - hand_processor = HandProcessor('1', 500, ['A', 'K'], ['S', 'H']) + hand_processor = HandProcessor("1", 500, ["A", "K"], ["S", "H"]) self.assertTrue(hand_processor.blackjack) self.assertFalse(hand_processor.aces_pair) - hand_processor = HandProcessor('1', 500, ['10', 'A'], ['S', 'H']) + hand_processor = HandProcessor("1", 500, ["10", "A"], ["S", "H"]) self.assertTrue(hand_processor.blackjack) - hand_processor = HandProcessor('1', 500, ['A', '9'], ['S', 'H']) + hand_processor = HandProcessor("1", 500, ["A", "9"], ["S", "H"]) self.assertFalse(hand_processor.blackjack) def test_aces_pair_detection(self): - hand_processor = HandProcessor('1', 500, ['A', 'A'], ['S', 'H']) + hand_processor = HandProcessor("1", 500, ["A", "A"], ["S", "H"]) self.assertTrue(hand_processor.aces_pair) self.assertFalse(hand_processor.blackjack) def test_soft_hand_value(self): - hand_processor = HandProcessor('1', 500, ['A', '7'], ['S', 'H']) - self.assertEqual(hand_processor.value_dict['1'], 18) - self.assertTrue(hand_processor.soft_dict['1']) - self.assertFalse(hand_processor.bust_dict['1']) + hand_processor = HandProcessor("1", 500, ["A", "7"], ["S", "H"]) + self.assertEqual(hand_processor.value_dict["1"], 18) + self.assertTrue(hand_processor.soft_dict["1"]) + self.assertFalse(hand_processor.bust_dict["1"]) def test_second_hand_ordinal(self): - hand_processor = HandProcessor('2', 1000, ['Q', '9'], ['D', 'C']) - self.assertEqual(hand_processor.head_ordinal, '2') + hand_processor = HandProcessor("2", 1000, ["Q", "9"], ["D", "C"]) + self.assertEqual(hand_processor.head_ordinal, "2") self.assertEqual(hand_processor.initial_chips, 1000) @@ -60,7 +60,7 @@ class TestSurrender(unittest.TestCase): """Test HandProcessor.surrender sets correct operations.""" def setUp(self): - self.hand_processor = HandProcessor('1', 500, ['Q', '6'], ['S', 'H']) + self.hand_processor = HandProcessor("1", 500, ["Q", "6"], ["S", "H"]) def test_surrender_flag(self): self.assertFalse(self.hand_processor.surrendered) @@ -69,105 +69,105 @@ def test_surrender_flag(self): def test_chips_immutability(self): self.hand_processor.surrender() - self.assertEqual(self.hand_processor.chips_dict['1'], 500) + self.assertEqual(self.hand_processor.chips_dict["1"], 500) def test_cards_immutability(self): self.hand_processor.surrender() - self.assertEqual(self.hand_processor.cards_dict['1'], ['Q', '6']) + self.assertEqual(self.hand_processor.cards_dict["1"], ["Q", "6"]) class TestHitOrDoubleDown(unittest.TestCase): """Test HandProcessor.hit_or_double_down correctly updates cards and state.""" def setUp(self): - self.hand_processor = HandProcessor('1', 500, ['7', '5'], ['S', 'H']) + self.hand_processor = HandProcessor("1", 500, ["7", "5"], ["S", "H"]) def test_hit_appends_card_and_suit(self): - self.hand_processor.hit_or_double_down('6', 'D') - self.assertEqual(self.hand_processor.cards_dict['1'], ['7', '5', '6']) - self.assertEqual(self.hand_processor.suits_dict['1'], ['S', 'H', 'D']) + self.hand_processor.hit_or_double_down("6", "D") + self.assertEqual(self.hand_processor.cards_dict["1"], ["7", "5", "6"]) + self.assertEqual(self.hand_processor.suits_dict["1"], ["S", "H", "D"]) def test_hit_updates_value(self): - self.hand_processor.hit_or_double_down('6', 'D') # 7 + 5 + 6 = 18. - self.assertEqual(self.hand_processor.value_dict['1'], 18) + self.hand_processor.hit_or_double_down("6", "D") # 7 + 5 + 6 = 18. + self.assertEqual(self.hand_processor.value_dict["1"], 18) def test_hit_bust_flag(self): - self.hand_processor.hit_or_double_down('K', 'D') # 7 + 5 + K = busted 22. - self.assertTrue(self.hand_processor.bust_dict['1']) + self.hand_processor.hit_or_double_down("K", "D") # 7 + 5 + K = busted 22. + self.assertTrue(self.hand_processor.bust_dict["1"]) def test_hit_no_bust_flag(self): - self.hand_processor.hit_or_double_down('4', 'D') # 7 + 5 + 4 = 16. Not busted. - self.assertFalse(self.hand_processor.bust_dict['1']) + self.hand_processor.hit_or_double_down("4", "D") # 7 + 5 + 4 = 16. Not busted. + self.assertFalse(self.hand_processor.bust_dict["1"]) def test_hit_chips_immutability(self): - self.hand_processor.hit_or_double_down('6', 'D') - self.assertEqual(self.hand_processor.chips_dict['1'], 500) - self.assertFalse(self.hand_processor.double_down_dict['1']) + self.hand_processor.hit_or_double_down("6", "D") + self.assertEqual(self.hand_processor.chips_dict["1"], 500) + self.assertFalse(self.hand_processor.double_down_dict["1"]) def test_double_down_doubles_chips(self): - self.hand_processor.hit_or_double_down('4', 'D', double_down=True) - self.assertEqual(self.hand_processor.chips_dict['1'], 1000) - self.assertTrue(self.hand_processor.double_down_dict['1']) + self.hand_processor.hit_or_double_down("4", "D", double_down=True) + self.assertEqual(self.hand_processor.chips_dict["1"], 1000) + self.assertTrue(self.hand_processor.double_down_dict["1"]) def test_double_down_bust_shows_double_chips(self): # 7 + 5 + K = busted 22. - self.hand_processor.hit_or_double_down('K', 'D', double_down=True) - self.assertTrue(self.hand_processor.bust_dict['1']) - self.assertEqual(self.hand_processor.chips_dict['1'], 1000) + self.hand_processor.hit_or_double_down("K", "D", double_down=True) + self.assertTrue(self.hand_processor.bust_dict["1"]) + self.assertEqual(self.hand_processor.chips_dict["1"], 1000) def test_hit_on_named_branch(self): - self.hand_processor.cards_dict['2'] = ['8'] - self.hand_processor.suits_dict['2'] = ['C'] - self.hand_processor.chips_dict['2'] = 500 + self.hand_processor.cards_dict["2"] = ["8"] + self.hand_processor.suits_dict["2"] = ["C"] + self.hand_processor.chips_dict["2"] = 500 - self.hand_processor.double_down_dict['2'] = False - self.hand_processor.hit_or_double_down('9', 'H', branch_ordinal='2') + self.hand_processor.double_down_dict["2"] = False + self.hand_processor.hit_or_double_down("9", "H", branch_ordinal="2") - self.assertEqual(self.hand_processor.cards_dict['2'], ['8', '9']) - self.assertEqual(self.hand_processor.value_dict['2'], 17) + self.assertEqual(self.hand_processor.cards_dict["2"], ["8", "9"]) + self.assertEqual(self.hand_processor.value_dict["2"], 17) def test_hit_soft_hand_turns_hard(self): - hand_processor = HandProcessor('1', 500, ['A', '5'], ['S', 'H']) - hand_processor.hit_or_double_down('K', 'D') # A + 5 + 10 = hard 16. - self.assertFalse(hand_processor.soft_dict['1']) - self.assertEqual(hand_processor.value_dict['1'], 16) + hand_processor = HandProcessor("1", 500, ["A", "5"], ["S", "H"]) + hand_processor.hit_or_double_down("K", "D") # A + 5 + 10 = hard 16. + self.assertFalse(hand_processor.soft_dict["1"]) + self.assertEqual(hand_processor.value_dict["1"], 16) class TestSplit(unittest.TestCase): """Test HandProcessor.split correctly creates a new branch and rearranges cards.""" def setUp(self): - self.hand_processor = HandProcessor('1', 500, ['8', '8'], ['S', 'H']) + self.hand_processor = HandProcessor("1", 500, ["8", "8"], ["S", "H"]) def test_split_creates_second_branch(self): - self.hand_processor.split('7', 'D') - self.assertIn('2', self.hand_processor.cards_dict) + self.hand_processor.split("7", "D") + self.assertIn("2", self.hand_processor.cards_dict) def test_first_branch_after_split(self): - self.hand_processor.split('9', 'C') - self.assertEqual(self.hand_processor.cards_dict['1'], ['8', '9']) + self.hand_processor.split("9", "C") + self.assertEqual(self.hand_processor.cards_dict["1"], ["8", "9"]) def test_split_second_branch_has_split_card(self): - self.hand_processor.split('9', 'C') - self.assertEqual(self.hand_processor.cards_dict['2'], ['8']) + self.hand_processor.split("9", "C") + self.assertEqual(self.hand_processor.cards_dict["2"], ["8"]) def test_splits_counter(self): - self.hand_processor.split('7', 'D') + self.hand_processor.split("7", "D") self.assertEqual(self.hand_processor.splits, 1) def test_split_updates_branch_value(self): - self.hand_processor.split('9', 'C') # 8 + 9' = 17. - self.assertEqual(self.hand_processor.value_dict['1'], 17) + self.hand_processor.split("9", "C") # 8 + 9' = 17. + self.assertEqual(self.hand_processor.value_dict["1"], 17) def test_split_moves_suit_to_second_branch(self): - self.hand_processor.split('9', 'C') + self.hand_processor.split("9", "C") # Second suit moved. - self.assertEqual(self.hand_processor.suits_dict['2'], ['H']) + self.assertEqual(self.hand_processor.suits_dict["2"], ["H"]) def test_aces_pair_split_marks_aces_pair(self): - hand_processor = HandProcessor('1', 500, ['A', 'A'], ['S', 'H']) - hand_processor.split('K', 'C') + hand_processor = HandProcessor("1", 500, ["A", "A"], ["S", "H"]) + hand_processor.split("K", "C") self.assertTrue(hand_processor.aces_pair) self.assertEqual(hand_processor.splits, 1) @@ -177,37 +177,37 @@ class TestReload(unittest.TestCase): """Test HandProcessor.reload fills the next split branch with a second card.""" def setUp(self): - self.hand_processor = HandProcessor('1', 500, ['8', '8'], ['S', 'H']) - self.hand_processor.split('J', 'C') # Branch 1 = ['8', 'J']. Branch 2 = ['8']. + self.hand_processor = HandProcessor("1", 500, ["8", "8"], ["S", "H"]) + self.hand_processor.split("J", "C") # Branch 1 = ['8', 'J']. Branch 2 = ['8']. def test_reload_appends_card_to_second_branch(self): - self.hand_processor.reload('5', 'D', '1') # next branch = '2' - self.assertEqual(self.hand_processor.cards_dict['2'], ['8', '5']) + self.hand_processor.reload("5", "D", "1") # next branch = '2' + self.assertEqual(self.hand_processor.cards_dict["2"], ["8", "5"]) def test_reload_appends_suit_to_second_branch(self): - self.hand_processor.reload('5', 'D', '1') - self.assertEqual(self.hand_processor.suits_dict['2'], ['H', 'D']) + self.hand_processor.reload("5", "D", "1") + self.assertEqual(self.hand_processor.suits_dict["2"], ["H", "D"]) def test_reload_sets_branch_chips_as_initial(self): - self.hand_processor.reload('5', 'D', '1') - self.assertEqual(self.hand_processor.chips_dict['2'], 500) + self.hand_processor.reload("5", "D", "1") + self.assertEqual(self.hand_processor.chips_dict["2"], 500) def test_reload_resets_double_down_flag(self): - self.hand_processor.reload('5', 'D', '1') - self.assertFalse(self.hand_processor.double_down_dict['2']) + self.hand_processor.reload("5", "D", "1") + self.assertFalse(self.hand_processor.double_down_dict["2"]) def test_reload_updates_branch_value(self): - self.hand_processor.reload('5', 'D', '1') # 8 + 5 = 13. - self.assertEqual(self.hand_processor.value_dict['2'], 13) + self.hand_processor.reload("5", "D", "1") # 8 + 5 = 13. + self.assertEqual(self.hand_processor.value_dict["2"], 13) def test_reload_nonexistent_branch(self): - self.hand_processor.reload('5', 'D', '2') # next branch '3' doesn't exist + self.hand_processor.reload("5", "D", "2") # next branch '3' doesn't exist def test_reload_branch_one_immutability(self): - cards_before = list(self.hand_processor.cards_dict['1']) - self.hand_processor.reload('5', 'D', '1') - self.assertEqual(self.hand_processor.cards_dict['1'], cards_before) + cards_before = list(self.hand_processor.cards_dict["1"]) + self.hand_processor.reload("5", "D", "1") + self.assertEqual(self.hand_processor.cards_dict["1"], cards_before) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/component/player_test.py b/tests/component/player_test.py index 061d0f0..cae6380 100644 --- a/tests/component/player_test.py +++ b/tests/component/player_test.py @@ -9,62 +9,62 @@ def setUp(self): self.player = Player() def test_single_hand_entry(self): - self.player.prepare([500], [['A', 'K', 'S', 'H']]) + self.player.prepare([500], [["A", "K", "S", "H"]]) self.assertEqual(len(self.player.hands_dict), 1) - self.assertIn('1', self.player.hands_dict) + self.assertIn("1", self.player.hands_dict) def test_multiple_hands_entries(self): - self.player.prepare([500, 600], [['A', 'K', 'S', 'H'], ['7', '8', 'D', 'C']]) + self.player.prepare([500, 600], [["A", "K", "S", "H"], ["7", "8", "D", "C"]]) self.assertEqual(len(self.player.hands_dict), 2) - self.assertIn('1', self.player.hands_dict) - self.assertIn('2', self.player.hands_dict) + self.assertIn("1", self.player.hands_dict) + self.assertIn("2", self.player.hands_dict) def test_clear_previous_hands(self): - self.player.prepare([500], [['A', 'K', 'S', 'H']]) - self.player.prepare([600], [['7', '8', 'D', 'C']]) + self.player.prepare([500], [["A", "K", "S", "H"]]) + self.player.prepare([600], [["7", "8", "D", "C"]]) self.assertEqual(len(self.player.hands_dict), 1) - self.assertEqual(self.player.hands_dict['1'].cards_dict['1'], ['7', '8']) + self.assertEqual(self.player.hands_dict["1"].cards_dict["1"], ["7", "8"]) def test_chips_assignments(self): - self.player.prepare([500], [['A', 'K', 'S', 'H']]) - self.assertEqual(self.player.hands_dict['1'].initial_chips, 500) + self.player.prepare([500], [["A", "K", "S", "H"]]) + self.assertEqual(self.player.hands_dict["1"].initial_chips, 500) self.player.prepare([300, 700], [["5", "6", "S", "H"], ["9", "9", "D", "C"]]) self.assertEqual(self.player.hands_dict["1"].initial_chips, 300) self.assertEqual(self.player.hands_dict["2"].initial_chips, 700) def test_cards_assignments(self): - self.player.prepare([500], [['7', '8', 'D', 'C']]) - self.assertEqual(self.player.hands_dict['1'].cards_dict['1'], ['7', '8']) + self.player.prepare([500], [["7", "8", "D", "C"]]) + self.assertEqual(self.player.hands_dict["1"].cards_dict["1"], ["7", "8"]) def test_suits_assignments(self): - self.player.prepare([500], [['7', '8', 'D', 'C']]) - self.assertEqual(self.player.hands_dict['1'].suits_dict['1'], ['D', 'C']) + self.player.prepare([500], [["7", "8", "D", "C"]]) + self.assertEqual(self.player.hands_dict["1"].suits_dict["1"], ["D", "C"]) def test_blackjack_detection(self): - self.player.prepare([500], [['A', 'K', 'S', 'H']]) - self.assertTrue(self.player.hands_dict['1'].blackjack) + self.player.prepare([500], [["A", "K", "S", "H"]]) + self.assertTrue(self.player.hands_dict["1"].blackjack) - self.player.prepare([500], [['7', '8', 'D', 'C']]) - self.assertFalse(self.player.hands_dict['1'].blackjack) + self.player.prepare([500], [["7", "8", "D", "C"]]) + self.assertFalse(self.player.hands_dict["1"].blackjack) def test_1_indexed_hand_ordinals(self): self.player.prepare( [500, 500, 500], [ - ['2', '3', 'S', 'H'], - ['4', '5', 'D', 'C'], - ['6', '7', 'S', 'D'], + ["2", "3", "S", "H"], + ["4", "5", "D", "C"], + ["6", "7", "S", "D"], ], ) - self.assertIn('1', self.player.hands_dict) - self.assertIn('2', self.player.hands_dict) - self.assertIn('3', self.player.hands_dict) + self.assertIn("1", self.player.hands_dict) + self.assertIn("2", self.player.hands_dict) + self.assertIn("3", self.player.hands_dict) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/integration/blackjack_test.py b/tests/integration/blackjack_test.py index 7b288cd..e29263c 100644 --- a/tests/integration/blackjack_test.py +++ b/tests/integration/blackjack_test.py @@ -65,12 +65,12 @@ def test_insufficient_capital_error(self): self.assertIn("error", result) def test_valid_bet_clears_error(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), - ('7', '8', 'D', 'H'), + ("6", "S"), + ("7", "8", "D", "H"), ], ): result = self.game.start_round([500]) @@ -86,13 +86,13 @@ def setUp(self): self.game.new_session("Alice", 10000) def test_phase_is_settled(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), # Dealer 1st card is 6. - ('7', '8', 'D', 'H'), # Player has 7 + 8 = 15. - ('K', 'C'), # Player hits K: 15 becomes busted 25. + ("6", "S"), # Dealer 1st card is 6. + ("7", "8", "D", "H"), # Player has 7 + 8 = 15. + ("K", "C"), # Player hits K: 15 becomes busted 25. ], ): @@ -102,13 +102,13 @@ def test_phase_is_settled(self): self.assertEqual(result["phase"], "settled") def test_busted_outcome(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), - ('9', '8', 'D', 'H'), - ('K', 'C'), + ("6", "S"), + ("9", "8", "D", "H"), + ("K", "C"), ], ): @@ -119,13 +119,13 @@ def test_busted_outcome(self): self.assertEqual(outcome, "bust") def test_capital_reduction(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), - ('9', '8', 'D', 'H'), - ('K', 'C'), + ("6", "S"), + ("9", "8", "D", "H"), + ("K", "C"), ], ): @@ -135,13 +135,13 @@ def test_capital_reduction(self): self.assertEqual(self.game.capital, 9500) def test_income_records_loss_entry(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), - ('9', '8', 'D', 'H'), - ('K', 'C'), + ("6", "S"), + ("9", "8", "D", "H"), + ("K", "C"), ], ): @@ -160,14 +160,14 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), # Dealer 1st card is 6. - ('9', '8', 'D', 'H'), # Player has 9 + 8 = 17. - ('8', 'C'), # Dealer draws 8: 6 becomes 14. - ('9', 'D'), # Dealer draws 9: 14 becomes busted 23. + ("6", "S"), # Dealer 1st card is 6. + ("9", "8", "D", "H"), # Player has 9 + 8 = 17. + ("8", "C"), # Dealer draws 8: 6 becomes 14. + ("9", "D"), # Dealer draws 9: 14 becomes busted 23. ], ): @@ -204,14 +204,14 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), # Dealer 1st card is 6. - ('K', '9', 'D', 'H'), # Player has K + 9 = 19. - ('K', 'C'), # Dealer draws K: 6 becomes 16. - ('3', 'D'), # Dealer draws 3: 16 becomes 19. + ("6", "S"), # Dealer 1st card is 6. + ("K", "9", "D", "H"), # Player has K + 9 = 19. + ("K", "C"), # Dealer draws K: 6 becomes 16. + ("3", "D"), # Dealer draws 3: 16 becomes 19. ], ): @@ -240,12 +240,12 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('6', 'S'), # Dealer 1st card is 9. - ('A', 'K', 'D', 'H'), # Player has A + K = Blackjack. + ("6", "S"), # Dealer 1st card is 9. + ("A", "K", "D", "H"), # Player has A + K = Blackjack. ], ): @@ -277,19 +277,19 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), # Dealer 1st card is A. - ('A', 'K', 'D', 'H'), # Player has A + K: Blackjack. + ("A", "S"), # Dealer 1st card is A. + ("A", "K", "D", "H"), # Player has A + K: Blackjack. ], ): self.game.start_round([500]) - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( - self.game.machine, 'draw', side_effect=[] + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, "draw", side_effect=[] ): return self.game.make_early_pay("take") @@ -311,12 +311,12 @@ def test_unit_profit(self): self.assertEqual(self.game.incomes[0]["Profit"], 500) def test_phase_is_marked_early_pay(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), - ('A', 'K', 'D', 'H'), + ("A", "S"), + ("A", "K", "D", "H"), ], ): result = self.game.start_round([500]) @@ -332,13 +332,13 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), # Dealer 1st card is A. - ('A', 'K', 'D', 'H'), # Player has A + K: Blackjack. - ('K', 'C'), # Dealer has A + K: Blackjack. + ("A", "S"), # Dealer 1st card is A. + ("A", "K", "D", "H"), # Player has A + K: Blackjack. + ("K", "C"), # Dealer has A + K: Blackjack. ], ): @@ -371,12 +371,12 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('J', 'S'), # Dealer 1st card is J. - ('Q', '6', 'D', 'H'), # Player has Q + 6 = 16. + ("J", "S"), # Dealer 1st card is J. + ("Q", "6", "D", "H"), # Player has Q + 6 = 16. ], ): @@ -401,12 +401,12 @@ def test_half_loss(self): self.assertEqual(self.game.incomes[0]["Profit"], -250) def test_surrender_in_available_moves(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('J', 'S'), - ('Q', '6', 'D', 'H'), + ("J", "S"), + ("Q", "6", "D", "H"), ], ): @@ -423,14 +423,14 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('8', 'S'), # Dealer 1st card is 8. - ('5', '6', 'D', 'H'), # Player has 5 + 6 = 11. - ('K', 'C'), # Double down draws K: 11 becomes 21. - ('Q', 'C'), # Dealer draws Q: 8 becomes 18. + ("8", "S"), # Dealer 1st card is 8. + ("5", "6", "D", "H"), # Player has 5 + 6 = 11. + ("K", "C"), # Double down draws K: 11 becomes 21. + ("Q", "C"), # Dealer draws Q: 8 becomes 18. ], ): @@ -464,12 +464,12 @@ def test_correct_profit(self): self.assertEqual(self.game.incomes[0]["Profit"], 1000) def test_double_down_in_available_moves(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('8', 'S'), - ('5', '6', 'D', 'H'), + ("8", "S"), + ("5", "6", "D", "H"), ], ): @@ -486,17 +486,17 @@ def setUp(self): self.game.new_session("Alice", 10000) def _run_round(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('5', 'S'), # Dealer 1st card is 5. - ('8', '8', 'D', 'H'), # Player has 8 + 8 pair. - ('J', 'C'), # Player splits: branch 1 = 8 + J = 18. - ('5', 'D'), # Reload branch 2 to 8 + 5 = 13. - ('2', 'S'), # Ineffective reload: branch 3 doesn't exist. - ('J', 'C'), # Dealer draws J: 5 becomes 15. - ('4', 'H'), # Dealer draws 4: 15 becomes 19. + ("5", "S"), # Dealer 1st card is 5. + ("8", "8", "D", "H"), # Player has 8 + 8 pair. + ("J", "C"), # Player splits: branch 1 = 8 + J = 18. + ("5", "D"), # Reload branch 2 to 8 + 5 = 13. + ("2", "S"), # Ineffective reload: branch 3 doesn't exist. + ("J", "C"), # Dealer draws J: 5 becomes 15. + ("4", "H"), # Dealer draws 4: 15 becomes 19. ], ): @@ -515,7 +515,7 @@ def test_two_branches_created(self): def test_split_counter(self): self._run_round() - hand = self.game.player.hands_dict['1'] + hand = self.game.player.hands_dict["1"] self.assertEqual(hand.splits, 1) def test_capital_reduction(self): @@ -535,12 +535,12 @@ def test_both_branches_lose(self): self.assertEqual(branches[1]["outcome"], "lost") def test_split_in_available_moves(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('5', 'S'), - ('8', '8', 'D', 'H'), + ("5", "S"), + ("8", "8", "D", "H"), ], ): @@ -557,12 +557,12 @@ def setUp(self): self.game.new_session("Alice", 10000) def test_insurance_phase_against_dealer_ace(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), # Dealer 1st card is A. - ('7', '8', 'D', 'H'), # Player doesn't have BJ. + ("A", "S"), # Dealer 1st card is A. + ("7", "8", "D", "H"), # Player doesn't have BJ. ], ): @@ -571,12 +571,12 @@ def test_insurance_phase_against_dealer_ace(self): self.assertEqual(result["phase"], "insurance") def test_insurance_hands(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), - ('7', '8', 'D', 'H'), + ("A", "S"), + ("7", "8", "D", "H"), ], ): result = self.game.start_round([500]) @@ -588,27 +588,27 @@ def test_insurance_hands(self): ) # 500 / 2. def test_capital_deduction(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), - ('7', '8', 'D', 'H'), + ("A", "S"), + ("7", "8", "D", "H"), ], ): self.game.start_round([500]) - self.game.make_insurance_decision(['1']) + self.game.make_insurance_decision(["1"]) self.assertEqual(self.game.capital, 9250) # 9500 - 250. def test_no_insurance_capital(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), - ('7', '8', 'D', 'H'), + ("A", "S"), + ("7", "8", "D", "H"), ], ): @@ -618,33 +618,33 @@ def test_no_insurance_capital(self): self.assertEqual(self.game.capital, 9500) def test_phase_after_insurance_decision(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), - ('7', '8', 'D', 'H'), + ("A", "S"), + ("7", "8", "D", "H"), ], ): self.game.start_round([500]) - result = self.game.make_insurance_decision(['1']) + result = self.game.make_insurance_decision(["1"]) self.assertEqual(result["phase"], "playing") def test_insurance_pays_2_to_1_against_dealer_blackjack(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), # Dealer 1st card is A. - ('10', '8', 'D', 'H'), # Player doesn't have BJ and buys insurance. - ('10', 'C'), # Dealer draws 10: Ace + 10 = Blackjack. + ("A", "S"), # Dealer 1st card is A. + ("10", "8", "D", "H"), # Player doesn't have BJ and buys insurance. + ("10", "C"), # Dealer draws 10: Ace + 10 = Blackjack. ], ): self.game.start_round([500]) - self.game.make_insurance_decision(['1']) + self.game.make_insurance_decision(["1"]) result = self.game.stand() # 10000 - 500 (bet) - 250 (insurance) = 9250. @@ -656,18 +656,18 @@ def test_insurance_pays_2_to_1_against_dealer_blackjack(self): self.assertEqual(result["phase"], "settled") def test_lost_hand_against_dealer_blackjack(self): - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), - ('7', '8', 'D', 'H'), - ('10', 'C'), + ("A", "S"), + ("7", "8", "D", "H"), + ("10", "C"), ], ): self.game.start_round([500]) - self.game.make_insurance_decision(['1']) + self.game.make_insurance_decision(["1"]) result = self.game.stand() self.assertEqual(result["hands"][0]["branches"][0]["outcome"], "lost") @@ -687,7 +687,7 @@ def test_lost_insurance_when_dealer_has_no_blackjack(self): ): self.game.start_round([500]) - self.game.make_insurance_decision(['1']) + self.game.make_insurance_decision(["1"]) result = self.game.stand() # 10000 - 500 (bet) - 250 (insurance) = 9250. @@ -698,12 +698,12 @@ def test_lost_insurance_when_dealer_has_no_blackjack(self): def test_insurance_skipped_when_all_hands_are_blackjack(self): """Dealer shows Ace but all player hands are BJ: goes to early pay, not insurance.""" - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( self.game.machine, - 'draw', + "draw", side_effect=[ - ('A', 'S'), # Dealer 1st card is Ace. - ('A', 'K', 'D', 'H'), # Player has Blackjack. + ("A", "S"), # Dealer 1st card is Ace. + ("A", "K", "D", "H"), # Player has Blackjack. ], ): @@ -713,7 +713,7 @@ def test_insurance_skipped_when_all_hands_are_blackjack(self): self.assertNotIn("insurance_hands", result) def test_make_insurance_decision_errors_outside_insurance_phase(self): - result = self.game.make_insurance_decision(['1']) + result = self.game.make_insurance_decision(["1"]) self.assertIn("error", result) @@ -726,11 +726,11 @@ def setUp(self): def test_round_number_increments(self): for draws in [ - [('9', 'S'), ('9', '5', 'D', 'H'), ('K', 'C')], # Round 1: bust. - [('9', 'S'), ('9', '4', 'D', 'H'), ('K', 'C')], # Round 2: bust. + [("9", "S"), ("9", "5", "D", "H"), ("K", "C")], # Round 1: bust. + [("9", "S"), ("9", "4", "D", "H"), ("K", "C")], # Round 2: bust. ]: - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( - self.game.machine, 'draw', side_effect=draws + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, "draw", side_effect=draws ): self.game.start_round([500]) self.game.hit() @@ -739,11 +739,11 @@ def test_round_number_increments(self): def test_incomes_accumulate_across_rounds(self): for draws in [ - [('9', 'S'), ('9', '5', 'D', 'H'), ('K', 'C')], # Round 1: bust. - [('9', 'S'), ('9', '4', 'D', 'H'), ('K', 'C')], # Round 2: bust. + [("9", "S"), ("9", "5", "D", "H"), ("K", "C")], # Round 1: bust. + [("9", "S"), ("9", "4", "D", "H"), ("K", "C")], # Round 2: bust. ]: - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( - self.game.machine, 'draw', side_effect=draws + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, "draw", side_effect=draws ): self.game.start_round([500]) @@ -753,11 +753,11 @@ def test_incomes_accumulate_across_rounds(self): def test_capital_carries_between_rounds(self): for draws in [ - [('6', 'S'), ('9', '8', 'D', 'H'), ('K', 'C')], - [('6', 'S'), ('9', '8', 'D', 'H'), ('K', 'C')], + [("6", "S"), ("9", "8", "D", "H"), ("K", "C")], + [("6", "S"), ("9", "8", "D", "H"), ("K", "C")], ]: - with patch.object(self.game.machine, 'load_and_shuffle'), patch.object( - self.game.machine, 'draw', side_effect=draws + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, "draw", side_effect=draws ): self.game.start_round([500]) @@ -766,5 +766,149 @@ def test_capital_carries_between_rounds(self): self.assertEqual(self.game.capital, 9000) # Lost 500 twice. -if __name__ == '__main__': +class TestHugeProfitCongrats(unittest.TestCase): + """Whole-round ROI congrats popup: fires once at settlement when net profit + over total committed chips (bets + doubles/splits + insurance) clears the + threshold. Payload rides in final settlement response.""" + + def setUp(self): + self.game = Blackjack() + self.game.new_session("Alice", 10000) + + def test_congrats_on_win(self): + # Player 17 stands; dealer 6 draws into bust. Net +500 on 500 staked. + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), + ("9", "8", "D", "H"), # Player 9 + 8 = 17. + ("8", "C"), # Dealer 6 -> 14. + ("9", "D"), # Dealer 14 -> bust 23. + ], + ): + self.game.start_round([500]) + result = self.game.stand() + + self.assertIn("congrats", result) + self.assertEqual(result["congrats"]["profit_rate"], 100.0) + + def test_congrats_profit_rate_on_blackjack(self): + # Player blackjack vs dealer low card auto-pays 1.5x: +750 on 500 staked. + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), # Dealer shows 6 -> no early pay, auto-pays BJ. + ("A", "K", "D", "H"), # Player blackjack. + ], + ): + result = self.game.start_round([500]) + + self.assertEqual(result["phase"], "settled") + self.assertIn("congrats", result) + self.assertEqual(result["congrats"]["profit_rate"], 150.0) + + def test_denominator_includes_doubled_wager(self): + # Double 500 -> 1000 committed; win via dealer bust pays +1000 net. + # Rate must be 1000/1000 = 100%, NOT 1000/500 = 200%. + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), + ("5", "6", "D", "H"), # Player 5 + 6 = 11. + ("9", "C"), # Double draw -> 20. + ("8", "D"), # Dealer 6 -> 14. + ("9", "H"), # Dealer 14 -> bust 23. + ], + ): + self.game.start_round([500]) + result = self.game.double_down() + + self.assertEqual(self.game.capital, 11000) + self.assertIn("congrats", result) + self.assertEqual(result["congrats"]["profit_rate"], 100.0) + + def test_no_congrats_on_push(self): + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), + ("K", "9", "D", "H"), # Player 19. + ("K", "C"), # Dealer 6 -> 16. + ("3", "D"), # Dealer 16 -> 19: push. + ], + ): + self.game.start_round([500]) + result = self.game.stand() + + self.assertNotIn("congrats", result) + + def test_no_congrats_on_loss(self): + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("K", "S"), + ("9", "8", "D", "H"), # Player 17. + ("J", "C"), # Dealer K -> 20, beats 17: player loses. + ], + ): + self.game.start_round([500]) + result = self.game.stand() + + self.assertNotIn("congrats", result) + + def test_no_congrats_key_before_settlement(self): + # The payload only exists once the whole round is finalized. + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), + ("9", "8", "D", "H"), + ("8", "C"), + ("9", "D"), + ], + ): + start = self.game.start_round([500]) + self.assertEqual(start["phase"], "playing") + self.assertNotIn("congrats", start) + + def test_congrats_resets_next_round(self): + # A huge-profit round followed by a flat round must not leak payload. + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), + ("9", "8", "D", "H"), + ("8", "C"), + ("9", "D"), + ], + ): + self.game.start_round([500]) + won = self.game.stand() + + self.assertIn("congrats", won) + + with patch.object(self.game.machine, "load_and_shuffle"), patch.object( + self.game.machine, + "draw", + side_effect=[ + ("6", "S"), + ("K", "9", "D", "H"), # Player 19. + ("K", "C"), # Dealer 6 -> 16. + ("3", "D"), # Dealer 16 -> 19: push. + ], + ): + self.game.start_round([500]) + pushed = self.game.stand() + + self.assertNotIn("congrats", pushed) + + +if __name__ == "__main__": unittest.main() diff --git a/tests/unit/trackers_test.py b/tests/unit/trackers_test.py index bcd960e..7b5264f 100644 --- a/tests/unit/trackers_test.py +++ b/tests/unit/trackers_test.py @@ -7,8 +7,8 @@ class TestShuffleMachine(unittest.TestCase): def test_update_properties(self): """Test functionality of update_properties.""" - self.assertEqual(update_properties(['A'] * 11), (21, True, False)) - self.assertEqual(update_properties(['A'] * 12), (12, False, False)) + self.assertEqual(update_properties(["A"] * 11), (21, True, False)) + self.assertEqual(update_properties(["A"] * 12), (12, False, False)) def test_track_display_value(self): self.assertEqual(track_display_value(18, soft=True, stand=True), "18") diff --git a/utils/judges.py b/utils/judges.py index 708193f..dfcbfa3 100644 --- a/utils/judges.py +++ b/utils/judges.py @@ -16,8 +16,8 @@ def judge_blackjack(cards_list: list[str], split: bool = False) -> bool: if split: # No Blackjack if split is conducted. return False - if len(cards_list) == 2 and 'A' in cards_list: - if len({'10', 'J', 'Q', 'K'} & set(cards_list)) == 1: + if len(cards_list) == 2 and "A" in cards_list: + if len({"10", "J", "Q", "K"} & set(cards_list)) == 1: return True return False @@ -36,7 +36,7 @@ def judge_surrender(cards_list: list[str], dealer_first_card: str, splits: int) bool: True if given hand can be surrendered, False otherwise. """ if len(cards_list) == 2 and splits == 0: - if SURRENDER_TO_ACE | (dealer_first_card != 'A'): + if SURRENDER_TO_ACE | (dealer_first_card != "A"): return True return False