Files
011-black-jack/src/blackjack/rules.py
T
Tirsvad 3c99040b96 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
2026-10-04 22:04:40 +08:00

79 lines
2.1 KiB
Python

"""!
@file rules.py
@brief Blackjack house rules as pure functions; no console input or output.
"""
import random
from collections.abc import Sequence
from enum import Enum
from blackjack.constants import (
ACE_HIGH,
ACE_LOW,
BLACKJACK_MARKER,
BLACKJACK_SCORE,
CARDS,
OPENING_HAND_SIZE,
)
class Outcome(Enum):
"""!
@brief How a game ended, seen from the player.
"""
DRAW = "draw"
WIN = "win"
WIN_BLACKJACK = "win_blackjack"
WIN_DEALER_BUST = "win_dealer_bust"
LOSE = "lose"
LOSE_DEALER_BLACKJACK = "lose_dealer_blackjack"
LOSE_BUST = "lose_bust"
def deal_card() -> int:
"""!
@brief Draw one card from the unlimited deck.
@return A card value; 11 is the ace and 10 covers 10, Jack, Queen and King.
"""
return random.choice(CARDS)
def calculate_score(cards: Sequence[int]) -> int:
"""!
@brief Score a hand.
@param cards The card values in the hand.
@return The total, or 0 for a blackjack (two cards: an ace and a 10).
An ace counts 11 unless that would bust the hand, then it counts 1.
"""
hand = list(cards)
total = sum(hand)
if len(hand) == OPENING_HAND_SIZE and total == BLACKJACK_SCORE:
return BLACKJACK_MARKER
while total > BLACKJACK_SCORE and ACE_HIGH in hand:
hand[hand.index(ACE_HIGH)] = ACE_LOW
total = sum(hand)
return total
def compare(user_score: int, computer_score: int) -> Outcome:
"""!
@brief Decide who won.
@param user_score The player's score (0 means blackjack).
@param computer_score The dealer's score (0 means blackjack).
@return The outcome seen from the player.
"""
if user_score == computer_score:
return Outcome.DRAW
if computer_score == BLACKJACK_MARKER:
return Outcome.LOSE_DEALER_BLACKJACK
if user_score == BLACKJACK_MARKER:
return Outcome.WIN_BLACKJACK
if user_score > BLACKJACK_SCORE:
return Outcome.LOSE_BUST
if computer_score > BLACKJACK_SCORE:
return Outcome.WIN_DEALER_BUST
if user_score > computer_score:
return Outcome.WIN
return Outcome.LOSE