diff --git a/src/snake_game/scoreboard.py b/src/snake_game/scoreboard.py index 650108f..7513098 100644 --- a/src/snake_game/scoreboard.py +++ b/src/snake_game/scoreboard.py @@ -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() diff --git a/tests/test_scoreboard.py b/tests/test_scoreboard.py index 68a7a08..a7e1c2b 100644 --- a/tests/test_scoreboard.py +++ b/tests/test_scoreboard.py @@ -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")