Author SHA1 Message Date
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
Tirsvad 3381855019 Merge pull request 'Add project setup: layout, pyproject, constants, Doxyfile, README' (#16) from mil-001-project-setup into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #16
2026-10-04 15:50:19 +02:00
10 changed files with 737 additions and 16 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ Leave the environment with `deactivate`.
## Run
The game entry point is added in the *Game Implementation* milestone:
Start the game from the activated virtual environment:
```bash
python -m blackjack
+21
View File
@@ -0,0 +1,21 @@
"""!
@file __main__.py
@brief Entry point: python -m blackjack.
"""
from blackjack.constants import MESSAGE_GOODBYE
from blackjack.game import play
def main() -> None:
"""!
@brief Run the game; leave quietly on Ctrl+C or Ctrl+D.
"""
try:
play()
except (EOFError, KeyboardInterrupt):
print(f"\n{MESSAGE_GOODBYE}")
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
"""!
@file art.py
@brief ASCII art shown at the start of every game.
"""
## The game logo from the course assignment.
LOGO: str = r"""
.------. _ _ _ _ _
|A_ _ |. | | | | | | (_) | |
|( \/ ).-----. | |__ | | __ _ ___| | ___ __ _ ___| | __
| \ /|K /\ | | '_ \| |/ _` |/ __| |/ / |/ _` |/ __| |/ /
| \/ | / \ | | |_) | | (_| | (__| <| | (_| | (__| <
`-----| \ / | |_.__/|_|\__,_|\___|_|\_\ |\__,_|\___|_|\_\\
| \/ K| _/ |
`------' |__/
"""
+46 -13
View File
@@ -24,17 +24,50 @@ BLACKJACK_MARKER: int = 0
## Unlimited deck: cards are never removed. 11 is the ace, 10 covers 10/J/Q/K.
CARDS: list[int] = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
## Emoji face for each card value.
CARD_FACES: dict[int, str] = {
ACE_HIGH: "\U0001f170️",
ACE_LOW: "\U0001f170️",
2: "2️⃣",
3: "3️⃣",
4: "4️⃣",
5: "5️⃣",
6: "6️⃣",
7: "7️⃣",
8: "8️⃣",
9: "9️⃣",
10: "\U0001f51f",
## Rank text for each card value; plain text renders in every terminal.
CARD_RANKS: dict[int, str] = {
ACE_HIGH: "A",
ACE_LOW: "A",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9",
10: "10",
}
## Suit emoji shown after the rank; cards in a hand cycle through them.
CARD_SUITS: tuple[str, ...] = ("♠️", "♥️", "♦️", "♣️")
## Answer that means yes at a yes/no prompt.
ANSWER_YES: str = "y"
## Answer that means no at a yes/no prompt.
ANSWER_NO: str = "n"
## Prompt asking the player whether to draw another card.
PROMPT_HIT: str = "Type 'y' to get another card, type 'n' to pass: "
## Prompt asking the player whether to play a game.
PROMPT_RESTART: str = "Do you want to play a game of Blackjack? Type 'y' or 'n': "
## Message shown after an answer that is neither yes nor no.
MESSAGE_INVALID_ANSWER: str = "Please answer 'y' or 'n'."
## Message shown when the player leaves the game.
MESSAGE_GOODBYE: str = "Goodbye!"
## Label shown instead of a score when a hand is a blackjack.
LABEL_BLACKJACK: str = "Blackjack"
## Outcome messages.
MESSAGE_DRAW: str = "Draw \U0001f643"
MESSAGE_WIN: str = "You win \U0001f603"
MESSAGE_WIN_BLACKJACK: str = "Win with a Blackjack \U0001f60e"
MESSAGE_WIN_DEALER_BUST: str = "Opponent went over. You win \U0001f601"
MESSAGE_LOSE: str = "You lose \U0001f624"
MESSAGE_LOSE_DEALER_BLACKJACK: str = "Lose, opponent has Blackjack \U0001f631"
MESSAGE_LOSE_BUST: str = "You went over. You lose \U0001f62d"
+61
View File
@@ -0,0 +1,61 @@
"""!
@file display.py
@brief Turns cards, scores and outcomes into text for the console.
"""
from collections.abc import Sequence
from blackjack.constants import (
BLACKJACK_MARKER,
CARD_RANKS,
CARD_SUITS,
LABEL_BLACKJACK,
MESSAGE_DRAW,
MESSAGE_LOSE,
MESSAGE_LOSE_BUST,
MESSAGE_LOSE_DEALER_BLACKJACK,
MESSAGE_WIN,
MESSAGE_WIN_BLACKJACK,
MESSAGE_WIN_DEALER_BUST,
)
from blackjack.rules import Outcome
_OUTCOME_MESSAGES: dict[Outcome, str] = {
Outcome.DRAW: MESSAGE_DRAW,
Outcome.WIN: MESSAGE_WIN,
Outcome.WIN_BLACKJACK: MESSAGE_WIN_BLACKJACK,
Outcome.WIN_DEALER_BUST: MESSAGE_WIN_DEALER_BUST,
Outcome.LOSE: MESSAGE_LOSE,
Outcome.LOSE_DEALER_BLACKJACK: MESSAGE_LOSE_DEALER_BLACKJACK,
Outcome.LOSE_BUST: MESSAGE_LOSE_BUST,
}
def render_hand(cards: Sequence[int]) -> str:
"""!
@brief Show a hand as emoji cards.
@param cards The card values in the hand.
@return The emoji faces separated by spaces.
"""
return " ".join(
f"{CARD_RANKS[card]}{CARD_SUITS[index % len(CARD_SUITS)]}"
for index, card in enumerate(cards)
)
def format_score(score: int) -> str:
"""!
@brief Show a score for the player.
@param score A score from calculate_score (0 means blackjack).
@return The number, or the blackjack label.
"""
return LABEL_BLACKJACK if score == BLACKJACK_MARKER else str(score)
def outcome_message(outcome: Outcome) -> str:
"""!
@brief Message announcing how the game ended.
@param outcome The outcome seen from the player.
@return The text to show.
"""
return _OUTCOME_MESSAGES[outcome]
+128
View File
@@ -0,0 +1,128 @@
"""!
@file game.py
@brief Console game flow: one game, the restart loop and console clearing.
"""
import os
from collections.abc import Callable
from blackjack.art import LOGO
from blackjack.constants import (
ANSWER_NO,
ANSWER_YES,
BLACKJACK_MARKER,
BLACKJACK_SCORE,
DEALER_STAND_SCORE,
MESSAGE_GOODBYE,
MESSAGE_INVALID_ANSWER,
OPENING_HAND_SIZE,
PROMPT_HIT,
PROMPT_RESTART,
)
from blackjack.display import format_score, outcome_message, render_hand
from blackjack.rules import Outcome, calculate_score, compare, deal_card
## Reads one answer from the player.
type Reader = Callable[[str], str]
## Shows one line to the player.
type Writer = Callable[[str], None]
## Draws one card.
type Draw = Callable[[], int]
def clear_console() -> None:
"""!
@brief Clear the terminal screen.
"""
os.system("cls" if os.name == "nt" else "clear")
def ask_yes_no(prompt: str, read: Reader, write: Writer) -> bool:
"""!
@brief Ask until the player answers yes or no.
@param prompt The question to show.
@param read Reads the player's answer.
@param write Shows a message to the player.
@return True for yes, False for no.
"""
while True:
answer = read(prompt).strip().lower()
if answer == ANSWER_YES:
return True
if answer == ANSWER_NO:
return False
write(MESSAGE_INVALID_ANSWER)
def play_game(read: Reader, write: Writer, draw: Draw) -> Outcome:
"""!
@brief Play one game: the player draws, then the dealer, then compare.
@param read Reads the player's answers.
@param write Shows text to the player.
@param draw Draws one card.
@return The outcome seen from the player.
"""
user_cards = [draw() for _ in range(OPENING_HAND_SIZE)]
computer_cards = [draw() for _ in range(OPENING_HAND_SIZE)]
computer_score = 0
user_score = 0
is_game_over = False
while not is_game_over:
user_score = calculate_score(user_cards)
computer_score = calculate_score(computer_cards)
write(
f" Your cards: {render_hand(user_cards)}, "
f"current score: {format_score(user_score)}"
)
write(f" Dealer's first card: {render_hand(computer_cards[:1])}")
if (
user_score == BLACKJACK_MARKER
or computer_score == BLACKJACK_MARKER
or user_score > BLACKJACK_SCORE
):
is_game_over = True
elif ask_yes_no(PROMPT_HIT, read, write):
user_cards.append(draw())
else:
is_game_over = True
is_user_bust = user_score > BLACKJACK_SCORE
while (
not is_user_bust
and computer_score != BLACKJACK_MARKER
and computer_score < DEALER_STAND_SCORE
):
computer_cards.append(draw())
computer_score = calculate_score(computer_cards)
write(
f" Your final hand: {render_hand(user_cards)}, "
f"final score: {format_score(user_score)}"
)
write(
f" Dealer's final hand: {render_hand(computer_cards)}, "
f"final score: {format_score(computer_score)}"
)
outcome = compare(user_score, computer_score)
write(outcome_message(outcome))
return outcome
def play(
read: Reader = input,
write: Writer = print,
draw: Draw = deal_card,
clear: Callable[[], None] = clear_console,
) -> None:
"""!
@brief Play games until the player does not want another one.
@param read Reads the player's answers.
@param write Shows text to the player.
@param draw Draws one card.
@param clear Clears the screen before each game.
"""
while ask_yes_no(PROMPT_RESTART, read, write):
clear()
write(LOGO)
play_game(read, write, draw)
write(MESSAGE_GOODBYE)
+78
View File
@@ -0,0 +1,78 @@
"""!
@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
+233
View File
@@ -0,0 +1,233 @@
"""!
@file test_game.py
@brief Tests for rendering and the console game flow, with scripted input.
"""
import ast
import inspect
import unittest
from collections.abc import Iterator
from blackjack import rules
from blackjack.art import LOGO
from blackjack.constants import (
CARD_SUITS,
DEALER_STAND_SCORE,
MESSAGE_GOODBYE,
MESSAGE_INVALID_ANSWER,
)
from blackjack.display import format_score, outcome_message, render_hand
from blackjack.game import ask_yes_no, play, play_game
from blackjack.rules import Outcome
class Script:
"""!
@brief Scripted console: queued answers, queued cards, recorded output.
"""
def __init__(self, answers: list[str], cards: list[int]) -> None:
"""!
@param answers The answers the player types, in order.
@param cards The cards drawn, in order.
"""
self._answers: Iterator[str] = iter(answers)
self._cards: Iterator[int] = iter(cards)
self.output: list[str] = []
self.clears = 0
def read(self, prompt: str) -> str:
"""!
@brief Next scripted answer.
"""
return next(self._answers)
def write(self, text: str) -> None:
"""!
@brief Record a line of output.
"""
self.output.append(text)
def draw(self) -> int:
"""!
@brief Next scripted card.
"""
return next(self._cards)
def clear(self) -> None:
"""!
@brief Count a console clear.
"""
self.clears += 1
@property
def text(self) -> str:
"""!
@brief All output as one string.
"""
return "\n".join(self.output)
class TestDisplay(unittest.TestCase):
"""!
@brief Tests for the rendering helpers.
"""
def test_render_hand_shows_rank_and_suit(self) -> None:
"""!
@brief Each card is its rank followed by a suit emoji, joined by spaces.
"""
self.assertEqual(
render_hand([11, 10, 7]),
f"A{CARD_SUITS[0]} 10{CARD_SUITS[1]} 7{CARD_SUITS[2]}",
)
def test_format_score_blackjack(self) -> None:
"""!
@brief Score 0 is shown as Blackjack, other scores as numbers.
"""
self.assertEqual(format_score(0), "Blackjack")
self.assertEqual(format_score(17), "17")
def test_every_outcome_has_a_message(self) -> None:
"""!
@brief No outcome is left without a message.
"""
for outcome in Outcome:
self.assertTrue(outcome_message(outcome))
class TestAskYesNo(unittest.TestCase):
"""!
@brief Tests for ask_yes_no.
"""
def test_accepts_yes_and_no(self) -> None:
"""!
@brief Answers are trimmed and case-insensitive.
"""
script = Script([" Y ", "N"], [])
self.assertTrue(ask_yes_no("?", script.read, script.write))
self.assertFalse(ask_yes_no("?", script.read, script.write))
def test_repeats_on_invalid_answer(self) -> None:
"""!
@brief An invalid answer is rejected and the question is asked again.
"""
script = Script(["maybe", "y"], [])
self.assertTrue(ask_yes_no("?", script.read, script.write))
self.assertEqual(script.output, [MESSAGE_INVALID_ANSWER])
class TestPlayGame(unittest.TestCase):
"""!
@brief Scripted games for each way a game can end.
"""
def run_game(self, answers: list[str], cards: list[int]) -> tuple[Outcome, Script]:
"""!
@brief Play one scripted game.
"""
script = Script(answers, cards)
outcome = play_game(script.read, script.write, script.draw)
return outcome, script
def test_stand_and_win(self) -> None:
"""!
@brief Stand on 19; dealer draws from 12 to 22 and busts.
"""
# user 10+9, dealer 10+2, dealer draws 10
outcome, _ = self.run_game(["n"], [10, 9, 10, 2, 10])
self.assertEqual(outcome, Outcome.WIN_DEALER_BUST)
def test_hit_then_bust(self) -> None:
"""!
@brief Hitting on 16 and drawing a 10 busts; the dealer does not draw.
"""
# user 10+6, dealer 10+7, user hits 10
outcome, script = self.run_game(["y"], [10, 6, 10, 7, 10])
self.assertEqual(outcome, Outcome.LOSE_BUST)
self.assertIn("Your final hand", script.text)
def test_player_blackjack_ends_without_prompt(self) -> None:
"""!
@brief A player blackjack ends the game before any question.
"""
outcome, _ = self.run_game([], [11, 10, 10, 9])
self.assertEqual(outcome, Outcome.WIN_BLACKJACK)
def test_dealer_blackjack(self) -> None:
"""!
@brief A dealer blackjack ends the game and the player loses.
"""
outcome, _ = self.run_game([], [10, 9, 11, 10])
self.assertEqual(outcome, Outcome.LOSE_DEALER_BLACKJACK)
def test_dealer_draws_until_stand_score(self) -> None:
"""!
@brief The dealer draws below 17 and stops at 17 or more.
"""
# user 10+8 stands; dealer 2+3, draws 4 (9), 5 (14), 3 (17)
outcome, script = self.run_game(["n"], [10, 8, 2, 3, 4, 5, 3])
self.assertEqual(outcome, Outcome.WIN)
self.assertIn(f"final score: {DEALER_STAND_SCORE}", script.text)
def test_draw(self) -> None:
"""!
@brief Equal final scores are a draw.
"""
outcome, _ = self.run_game(["n"], [10, 8, 10, 8])
self.assertEqual(outcome, Outcome.DRAW)
class TestPlay(unittest.TestCase):
"""!
@brief Scripted end-to-end sessions including restart.
"""
def test_full_session_with_restart(self) -> None:
"""!
@brief Two games: hit-and-stand, then a restart answer of no.
"""
script = Script(
answers=["y", "y", "n", "y", "n"],
# game 1: user 5+5, dealer 10+8; user hits 10 (20), stands; dealer stands 18
# game 2: blackjack for the player
cards=[5, 5, 10, 8, 10, 11, 10, 10, 9],
)
play(script.read, script.write, script.draw, script.clear)
self.assertEqual(script.clears, 2)
self.assertEqual(script.text.count(LOGO), 2)
self.assertIn(outcome_message(Outcome.WIN), script.text)
self.assertIn(outcome_message(Outcome.WIN_BLACKJACK), script.text)
self.assertEqual(script.output[-1], MESSAGE_GOODBYE)
def test_quit_straight_away(self) -> None:
"""!
@brief Answering no at the first prompt plays no game.
"""
script = Script(["n"], [])
play(script.read, script.write, script.draw, script.clear)
self.assertEqual(script.clears, 0)
class TestRulesAreFreeOfConsoleIo(unittest.TestCase):
"""!
@brief The rule functions must not read or print.
"""
def test_no_input_or_print_in_rules(self) -> None:
"""!
@brief blackjack.rules contains no call to input() or print().
"""
tree = ast.parse(inspect.getsource(rules))
called = {
node.func.id
for node in ast.walk(tree)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
self.assertFalse(called & {"input", "print"})
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -28,10 +28,10 @@ class TestPackage(unittest.TestCase):
def test_every_card_has_an_emoji_face(self) -> None:
"""!
@brief Every card in the deck can be rendered.
@brief Every card in the deck has a rank to render.
"""
for card in constants.CARDS:
self.assertIn(card, constants.CARD_FACES)
self.assertIn(card, constants.CARD_RANKS)
if __name__ == "__main__":
+151
View File
@@ -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()