Add the high score to the scoreboard and replace game_over with reset

Refs #17
Refs #18
Task: MIL-003#2
Task: MIL-003#3
This commit is contained in:
2026-10-13 14:33:54 +08:00
parent 63090fce3d
commit 518133b688
2 changed files with 198 additions and 143 deletions
+61 -58
View File
@@ -1,58 +1,61 @@
"""! @file """! @file
@brief The scoreboard: the text at the top of the screen that shows the score. @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` 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; 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` the game imports it only when it starts, and the tests install a fake `turtle`
module first. module first.
""" """
from turtle import Turtle from turtle import Turtle
from snake_game.constants import ( from snake_game.constants import (
GAME_OVER_POSITION, HIGH_SCORE_LABEL,
GAME_OVER_TEXT, SCORE_LABEL,
SCORE_LABEL, SCOREBOARD_ALIGNMENT,
SCOREBOARD_ALIGNMENT, SCOREBOARD_COLOR,
SCOREBOARD_COLOR, SCOREBOARD_FONT,
SCOREBOARD_FONT, SCOREBOARD_POSITION,
SCOREBOARD_POSITION, )
)
class Scoreboard(Turtle):
class Scoreboard(Turtle): """! @brief Shows the score as text at the top centre of the screen."""
"""! @brief Shows the score as text at the top centre of the screen."""
def __init__(self) -> None:
def __init__(self) -> None: """! @brief Set the scoreboard up and write the score 0."""
"""! @brief Set the scoreboard up and write the score 0.""" super().__init__()
super().__init__() ## @brief The number of foods the snake has eaten since the last reset.
## @brief The number of foods the snake has eaten in this game. self.score = 0
self.score = 0 ## @brief The largest score reached so far.
self.color(SCOREBOARD_COLOR) self.high_score = 0
self.penup() self.color(SCOREBOARD_COLOR)
self.hideturtle() self.penup()
self.goto(SCOREBOARD_POSITION) self.hideturtle()
self.update_scoreboard() self.goto(SCOREBOARD_POSITION)
self.update_scoreboard()
def update_scoreboard(self) -> None:
"""! @brief Wipe the old text and write the label and the score.""" def update_scoreboard(self) -> None:
self.clear() """! @brief Wipe the old text and write the score and the high score."""
self.write( self.clear()
f"{SCORE_LABEL}{self.score}", self.write(
align=SCOREBOARD_ALIGNMENT, f"{SCORE_LABEL}{self.score} {HIGH_SCORE_LABEL}{self.high_score}",
font=SCOREBOARD_FONT, align=SCOREBOARD_ALIGNMENT,
) font=SCOREBOARD_FONT,
)
def increase_score(self) -> None:
"""! @brief Add 1 to the score and write it.""" def increase_score(self) -> None:
self.score += 1 """! @brief Add 1 to the score and write it."""
self.update_scoreboard() self.score += 1
self.update_scoreboard()
def game_over(self) -> None:
"""! @brief Write the game-over text at the centre of the screen. def reset(self) -> None:
"""! @brief Keep the best score, start a new game and write the scoreboard.
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. When the score is higher than the high score, it becomes the new high
""" score. The score then goes back to 0.
self.goto(GAME_OVER_POSITION) """
self.write(GAME_OVER_TEXT, align=SCOREBOARD_ALIGNMENT, font=SCOREBOARD_FONT) if self.score > self.high_score:
self.high_score = self.score
self.score = 0
self.update_scoreboard()
+137 -85
View File
@@ -1,85 +1,137 @@
"""Tests of the scoreboard, with a fake `Turtle` as its base class.""" """Tests of the scoreboard, with a fake `Turtle` as its base class."""
import importlib import importlib
import sys import sys
import pytest import pytest
from fakes import install_fake_turtle, make_scoreboard from fakes import install_fake_turtle, make_scoreboard
from snake_game import constants from snake_game import constants
START_TEXT = ("write", ("Score: 0", "center", ("Arial", 24, "normal"))) START_TEXT = (
"write",
("Score: 0 High 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) def test_scoreboard_is_white_hidden_and_at_the_top_centre(
monkeypatch: pytest.MonkeyPatch,
assert ("color", ("white",)) in scoreboard.calls ) -> None:
assert "penup" in scoreboard.call_names() scoreboard = make_scoreboard(monkeypatch)
assert "hideturtle" in scoreboard.call_names()
assert ("goto", constants.SCOREBOARD_POSITION) in scoreboard.calls assert ("color", ("white",)) in scoreboard.calls
assert "penup" in scoreboard.call_names()
assert "hideturtle" in scoreboard.call_names()
def test_scoreboard_inherits_from_turtle(monkeypatch: pytest.MonkeyPatch) -> None: assert ("goto", constants.SCOREBOARD_POSITION) in scoreboard.calls
install_fake_turtle(monkeypatch)
module = importlib.import_module("snake_game.scoreboard")
def test_scoreboard_inherits_from_turtle(monkeypatch: pytest.MonkeyPatch) -> None:
assert issubclass(module.Scoreboard, sys.modules["turtle"].Turtle) install_fake_turtle(monkeypatch)
module = importlib.import_module("snake_game.scoreboard")
def test_scoreboard_starts_by_writing_score_0_centred_in_arial_24( assert issubclass(module.Scoreboard, sys.modules["turtle"].Turtle)
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch) def test_scoreboard_starts_by_writing_both_scores_0_centred_in_arial_24(
monkeypatch: pytest.MonkeyPatch,
assert scoreboard.calls[-1] == START_TEXT ) -> None:
scoreboard = make_scoreboard(monkeypatch)
def test_increase_score_wipes_the_old_text_and_writes_the_new_score( assert scoreboard.calls[-1] == START_TEXT
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch) def test_increase_score_wipes_the_old_text_and_writes_the_new_score(
monkeypatch: pytest.MonkeyPatch,
scoreboard.increase_score() ) -> None:
scoreboard = make_scoreboard(monkeypatch)
assert scoreboard.call_names()[-2:] == ["clear", "write"]
assert scoreboard.calls[-1] == ( scoreboard.increase_score()
"write",
("Score: 1", "center", ("Arial", 24, "normal")), assert scoreboard.call_names()[-2:] == ["clear", "write"]
) assert scoreboard.calls[-1] == (
"write",
("Score: 1 High Score: 0", "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): def test_every_food_adds_one_to_the_score(monkeypatch: pytest.MonkeyPatch) -> None:
scoreboard.increase_score() scoreboard = make_scoreboard(monkeypatch)
assert scoreboard.calls[-1][1][0] == "Score: 3" for _ in range(3):
scoreboard.increase_score()
def test_game_over_writes_the_text_at_the_centre_in_the_scoreboard_font( assert scoreboard.calls[-1][1][0] == "Score: 3 High Score: 0"
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch) def test_high_score_starts_at_0(monkeypatch: pytest.MonkeyPatch) -> None:
scoreboard = make_scoreboard(monkeypatch)
scoreboard.game_over()
assert scoreboard.high_score == 0
assert scoreboard.calls[-2:] == [ assert scoreboard.score == 0
("goto", constants.GAME_OVER_POSITION),
("write", ("GAME OVER", "center", ("Arial", 24, "normal"))),
] def test_reset_keeps_a_higher_score_as_the_high_score_and_starts_again(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def test_game_over_leaves_the_score_on_the_screen( scoreboard = make_scoreboard(monkeypatch)
monkeypatch: pytest.MonkeyPatch, for _ in range(3):
) -> None: scoreboard.increase_score()
scoreboard = make_scoreboard(monkeypatch)
scoreboard.increase_score() scoreboard.reset()
calls_before = len(scoreboard.calls)
assert scoreboard.high_score == 3
scoreboard.game_over() assert scoreboard.score == 0
assert scoreboard.call_names()[-2:] == ["clear", "write"]
assert "clear" not in scoreboard.call_names()[calls_before:] assert scoreboard.calls[-1] == (
"write",
("Score: 0 High Score: 3", "center", ("Arial", 24, "normal")),
)
def test_reset_keeps_the_high_score_when_the_score_is_lower(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch)
for _ in range(3):
scoreboard.increase_score()
scoreboard.reset()
scoreboard.increase_score()
scoreboard.reset()
assert scoreboard.high_score == 3
assert scoreboard.calls[-1][1][0] == "Score: 0 High Score: 3"
def test_reset_keeps_the_high_score_when_the_score_is_equal(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch)
scoreboard.increase_score()
scoreboard.reset()
scoreboard.increase_score()
scoreboard.reset()
assert scoreboard.high_score == 1
assert scoreboard.score == 0
def test_the_score_text_shows_both_scores_while_playing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch)
scoreboard.increase_score()
scoreboard.increase_score()
scoreboard.reset()
scoreboard.increase_score()
assert scoreboard.calls[-1][1][0] == "Score: 1 High Score: 2"
def test_scoreboard_has_no_game_over_any_more(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch)
assert not hasattr(scoreboard, "game_over")