From ca65ba7830e345a36d3eac2fa1ae5d1216070903 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Thu, 8 Oct 2026 18:31:05 +0800 Subject: [PATCH] Adopt the Scoreboard class and its tests Copied unchanged from the base: class Scoreboard(Turtle) with update_scoreboard, increase_score and game_over. MIL-003 checks the values against the day-21 lectures. Task: MIL-002#4 Closes #11 --- src/snake_game/scoreboard.py | 58 ++++++++++++++++++++++++ tests/test_scoreboard.py | 85 ++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/snake_game/scoreboard.py create mode 100644 tests/test_scoreboard.py diff --git a/src/snake_game/scoreboard.py b/src/snake_game/scoreboard.py new file mode 100644 index 0000000..650108f --- /dev/null +++ b/src/snake_game/scoreboard.py @@ -0,0 +1,58 @@ +"""! @file +@brief The scoreboard: the text at the top of the screen that shows the score. + +The class follows the day-21 lecture on inheritance: `Scoreboard` is a `Turtle` +that writes text. Like `food`, this module needs `turtle` when it is imported; +the game imports it only when it starts, and the tests install a fake `turtle` +module first. +""" + +from turtle import Turtle + +from snake_game.constants import ( + GAME_OVER_POSITION, + GAME_OVER_TEXT, + SCORE_LABEL, + SCOREBOARD_ALIGNMENT, + SCOREBOARD_COLOR, + SCOREBOARD_FONT, + SCOREBOARD_POSITION, +) + + +class Scoreboard(Turtle): + """! @brief Shows the score as text at the top centre of the screen.""" + + def __init__(self) -> None: + """! @brief Set the scoreboard up and write the score 0.""" + super().__init__() + ## @brief The number of foods the snake has eaten in this game. + self.score = 0 + self.color(SCOREBOARD_COLOR) + self.penup() + self.hideturtle() + self.goto(SCOREBOARD_POSITION) + self.update_scoreboard() + + def update_scoreboard(self) -> None: + """! @brief Wipe the old text and write the label and the score.""" + self.clear() + self.write( + f"{SCORE_LABEL}{self.score}", + align=SCOREBOARD_ALIGNMENT, + font=SCOREBOARD_FONT, + ) + + def increase_score(self) -> None: + """! @brief Add 1 to the score and write it.""" + self.score += 1 + self.update_scoreboard() + + def game_over(self) -> None: + """! @brief Write the game-over text at the centre of the screen. + + The score stays where it is: the old text is not wiped. Automatic drawing is + off in the game, so the screen must be updated after this call. + """ + self.goto(GAME_OVER_POSITION) + self.write(GAME_OVER_TEXT, align=SCOREBOARD_ALIGNMENT, font=SCOREBOARD_FONT) diff --git a/tests/test_scoreboard.py b/tests/test_scoreboard.py new file mode 100644 index 0000000..68a7a08 --- /dev/null +++ b/tests/test_scoreboard.py @@ -0,0 +1,85 @@ +"""Tests of the scoreboard, with a fake `Turtle` as its base class.""" + +import importlib +import sys + +import pytest + +from fakes import install_fake_turtle, make_scoreboard +from snake_game import constants + +START_TEXT = ("write", ("Score: 0", "center", ("Arial", 24, "normal"))) + + +def test_scoreboard_is_white_hidden_and_at_the_top_centre( + monkeypatch: pytest.MonkeyPatch, +) -> None: + scoreboard = make_scoreboard(monkeypatch) + + assert ("color", ("white",)) in scoreboard.calls + assert "penup" in scoreboard.call_names() + assert "hideturtle" in scoreboard.call_names() + assert ("goto", constants.SCOREBOARD_POSITION) in scoreboard.calls + + +def test_scoreboard_inherits_from_turtle(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_turtle(monkeypatch) + module = importlib.import_module("snake_game.scoreboard") + + assert issubclass(module.Scoreboard, sys.modules["turtle"].Turtle) + + +def test_scoreboard_starts_by_writing_score_0_centred_in_arial_24( + monkeypatch: pytest.MonkeyPatch, +) -> None: + scoreboard = make_scoreboard(monkeypatch) + + assert scoreboard.calls[-1] == START_TEXT + + +def test_increase_score_wipes_the_old_text_and_writes_the_new_score( + monkeypatch: pytest.MonkeyPatch, +) -> None: + scoreboard = make_scoreboard(monkeypatch) + + scoreboard.increase_score() + + assert scoreboard.call_names()[-2:] == ["clear", "write"] + assert scoreboard.calls[-1] == ( + "write", + ("Score: 1", "center", ("Arial", 24, "normal")), + ) + + +def test_every_food_adds_one_to_the_score(monkeypatch: pytest.MonkeyPatch) -> None: + scoreboard = make_scoreboard(monkeypatch) + + for _ in range(3): + scoreboard.increase_score() + + assert scoreboard.calls[-1][1][0] == "Score: 3" + + +def test_game_over_writes_the_text_at_the_centre_in_the_scoreboard_font( + monkeypatch: pytest.MonkeyPatch, +) -> None: + scoreboard = make_scoreboard(monkeypatch) + + scoreboard.game_over() + + assert scoreboard.calls[-2:] == [ + ("goto", constants.GAME_OVER_POSITION), + ("write", ("GAME OVER", "center", ("Arial", 24, "normal"))), + ] + + +def test_game_over_leaves_the_score_on_the_screen( + monkeypatch: pytest.MonkeyPatch, +) -> None: + scoreboard = make_scoreboard(monkeypatch) + scoreboard.increase_score() + calls_before = len(scoreboard.calls) + + scoreboard.game_over() + + assert "clear" not in scoreboard.call_names()[calls_before:]