Add Blackjack game: rules, card rendering, game loop and tests
Implement the playable console game for gateway MIL-002. - rules.py: deal_card, calculate_score (blackjack = 0, aces drop 11 -> 1) and compare with an Outcome enum; no console I/O - display.py: hand rendering as plain-text rank plus suit emoji (keycap emoji rendered as boxes in common terminals), score formatting and outcome messages - art.py: course logo shown at the start of every game - game.py: play_game (hit/stand, dealer draws below 17), play (restart loop, console clear, logo) and ask_yes_no with re-prompt; read, write and draw are injected so games can be scripted - __main__.py: python -m blackjack entry point, exits cleanly on Ctrl+C/Ctrl+D - constants.py: card ranks and suits, prompts and messages - tests: 32 new unit and scripted end-to-end tests (35 in total) - README: run instructions for python -m blackjack Closes #7 Closes #8 Closes #9 Closes #10 Closes #11 Closes #12 Closes #13 Closes #14 Task: MIL-002#1 Task: MIL-002#2 Task: MIL-002#3 Task: MIL-002#4 Task: MIL-002#5 Task: MIL-002#6 Task: MIL-002#7 Task: MIL-002#8
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""!
|
||||
@file test_rules.py
|
||||
@brief Unit tests for the house rules in blackjack.rules.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from blackjack.constants import CARDS
|
||||
from blackjack.rules import Outcome, calculate_score, compare, deal_card
|
||||
|
||||
|
||||
class TestDealCard(unittest.TestCase):
|
||||
"""!
|
||||
@brief Tests for deal_card.
|
||||
"""
|
||||
|
||||
def test_returns_card_from_deck(self) -> None:
|
||||
"""!
|
||||
@brief Every draw is a card of the deck.
|
||||
"""
|
||||
for _ in range(200):
|
||||
self.assertIn(deal_card(), CARDS)
|
||||
|
||||
def test_uses_random_choice_on_the_deck(self) -> None:
|
||||
"""!
|
||||
@brief The card comes from random.choice over the deck constant.
|
||||
"""
|
||||
with patch("blackjack.rules.random.choice", return_value=7) as choice:
|
||||
self.assertEqual(deal_card(), 7)
|
||||
choice.assert_called_once_with(CARDS)
|
||||
|
||||
def test_deck_is_not_consumed(self) -> None:
|
||||
"""!
|
||||
@brief Drawing does not remove cards from the deck.
|
||||
"""
|
||||
before = list(CARDS)
|
||||
for _ in range(50):
|
||||
deal_card()
|
||||
self.assertEqual(CARDS, before)
|
||||
|
||||
|
||||
class TestCalculateScore(unittest.TestCase):
|
||||
"""!
|
||||
@brief Tests for calculate_score.
|
||||
"""
|
||||
|
||||
def test_normal_total(self) -> None:
|
||||
"""!
|
||||
@brief A plain hand scores the sum of its cards.
|
||||
"""
|
||||
self.assertEqual(calculate_score([2, 3, 9]), 14)
|
||||
|
||||
def test_blackjack_scores_zero(self) -> None:
|
||||
"""!
|
||||
@brief Ace plus 10 in two cards is a blackjack, in either order.
|
||||
"""
|
||||
self.assertEqual(calculate_score([11, 10]), 0)
|
||||
self.assertEqual(calculate_score([10, 11]), 0)
|
||||
|
||||
def test_three_card_21_is_not_blackjack(self) -> None:
|
||||
"""!
|
||||
@brief 21 with more than two cards is a normal 21.
|
||||
"""
|
||||
self.assertEqual(calculate_score([7, 7, 7]), 21)
|
||||
|
||||
def test_ace_counts_one_when_hand_would_bust(self) -> None:
|
||||
"""!
|
||||
@brief An ace drops from 11 to 1 when the total exceeds 21.
|
||||
"""
|
||||
self.assertEqual(calculate_score([11, 5, 10]), 16)
|
||||
|
||||
def test_two_aces(self) -> None:
|
||||
"""!
|
||||
@brief Two aces score 12: one counts 11, the other 1.
|
||||
"""
|
||||
self.assertEqual(calculate_score([11, 11]), 12)
|
||||
|
||||
def test_only_needed_aces_are_demoted(self) -> None:
|
||||
"""!
|
||||
@brief Aces drop one at a time, only until the hand no longer busts.
|
||||
"""
|
||||
self.assertEqual(calculate_score([11, 11, 9]), 21)
|
||||
|
||||
def test_bust(self) -> None:
|
||||
"""!
|
||||
@brief A hand over 21 without aces keeps its total.
|
||||
"""
|
||||
self.assertEqual(calculate_score([10, 10, 5]), 25)
|
||||
|
||||
def test_does_not_change_the_hand(self) -> None:
|
||||
"""!
|
||||
@brief Scoring leaves the caller's list unchanged.
|
||||
"""
|
||||
hand = [11, 5, 10]
|
||||
calculate_score(hand)
|
||||
self.assertEqual(hand, [11, 5, 10])
|
||||
|
||||
|
||||
class TestCompare(unittest.TestCase):
|
||||
"""!
|
||||
@brief Tests for compare, in the order of the house rules.
|
||||
"""
|
||||
|
||||
def test_equal_scores_draw(self) -> None:
|
||||
"""!
|
||||
@brief Equal scores are a draw, also two blackjacks.
|
||||
"""
|
||||
self.assertEqual(compare(18, 18), Outcome.DRAW)
|
||||
self.assertEqual(compare(0, 0), Outcome.DRAW)
|
||||
|
||||
def test_dealer_blackjack_loses(self) -> None:
|
||||
"""!
|
||||
@brief A dealer blackjack beats the player.
|
||||
"""
|
||||
self.assertEqual(compare(20, 0), Outcome.LOSE_DEALER_BLACKJACK)
|
||||
|
||||
def test_user_blackjack_wins(self) -> None:
|
||||
"""!
|
||||
@brief A player blackjack wins.
|
||||
"""
|
||||
self.assertEqual(compare(0, 20), Outcome.WIN_BLACKJACK)
|
||||
|
||||
def test_user_bust_loses(self) -> None:
|
||||
"""!
|
||||
@brief A player over 21 loses.
|
||||
"""
|
||||
self.assertEqual(compare(22, 18), Outcome.LOSE_BUST)
|
||||
|
||||
def test_user_bust_loses_even_when_dealer_busts(self) -> None:
|
||||
"""!
|
||||
@brief The player's bust is checked before the dealer's.
|
||||
"""
|
||||
self.assertEqual(compare(23, 25), Outcome.LOSE_BUST)
|
||||
|
||||
def test_dealer_bust_wins(self) -> None:
|
||||
"""!
|
||||
@brief A dealer over 21 loses.
|
||||
"""
|
||||
self.assertEqual(compare(15, 24), Outcome.WIN_DEALER_BUST)
|
||||
|
||||
def test_higher_score_wins(self) -> None:
|
||||
"""!
|
||||
@brief Otherwise the higher score wins.
|
||||
"""
|
||||
self.assertEqual(compare(19, 18), Outcome.WIN)
|
||||
self.assertEqual(compare(17, 20), Outcome.LOSE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user