MIL-003: Continuous play and high score #37

Merged
Tirsvad merged 7 commits from mil-003-continuous-play-and-high-score into main 2026-10-13 08:35:54 +02:00
2 changed files with 198 additions and 143 deletions
Showing only changes of commit 518133b688 - Show all commits
+61 -58
View File
@@ -1,58 +1,61 @@
"""! @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)
"""! @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 (
HIGH_SCORE_LABEL,
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 since the last reset.
self.score = 0
## @brief The largest score reached so far.
self.high_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 score and the high score."""
self.clear()
self.write(
f"{SCORE_LABEL}{self.score} {HIGH_SCORE_LABEL}{self.high_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 reset(self) -> None:
"""! @brief Keep the best score, start a new game and write the scoreboard.
When the score is higher than the high score, it becomes the new high
score. The score then goes back to 0.
"""
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."""
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:]
"""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 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)
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_both_scores_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 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):
scoreboard.increase_score()
assert scoreboard.calls[-1][1][0] == "Score: 3 High Score: 0"
def test_high_score_starts_at_0(monkeypatch: pytest.MonkeyPatch) -> None:
scoreboard = make_scoreboard(monkeypatch)
assert scoreboard.high_score == 0
assert scoreboard.score == 0
def test_reset_keeps_a_higher_score_as_the_high_score_and_starts_again(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scoreboard = make_scoreboard(monkeypatch)
for _ in range(3):
scoreboard.increase_score()
scoreboard.reset()
assert scoreboard.high_score == 3
assert scoreboard.score == 0
assert scoreboard.call_names()[-2:] == ["clear", "write"]
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")