MIL-003: Continuous play and high score #37
@@ -19,11 +19,12 @@ This repository continues
|
|||||||
and starts from its finished game; `docs/project-plan.md` tells how the work is
|
and starts from its finished game; `docs/project-plan.md` tells how the work is
|
||||||
split into milestones.
|
split into milestones.
|
||||||
|
|
||||||
> **Status:** in progress. The finished game of day 21 is adopted and runs: the
|
> **Status:** in progress. The game of day 21 is adopted and now follows the
|
||||||
> snake moves, eats and grows, the score is shown and the game ends at the wall
|
> day-24 lectures on continuous play: the scoreboard shows the score and the high
|
||||||
> or at the tail. Continuous play and the high score (milestone 3), the high
|
> score, and a collision resets the snake and the score instead of ending the
|
||||||
> score file (milestone 4) and the wiki (milestone 5) follow. The sections below
|
> game. The high score is kept in memory only; the high score file (milestone 4)
|
||||||
> describe the game as it is now; they are updated in each milestone.
|
> and the wiki (milestone 5) follow. The sections below describe the game as it
|
||||||
|
> is now; they are updated in each milestone.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -104,11 +105,16 @@ closer to it than 15 pixels the snake eats it: the food moves to a new random
|
|||||||
place, the snake grows by one segment, and the score at the top of the window
|
place, the snake grows by one segment, and the score at the top of the window
|
||||||
goes up by 1.
|
goes up by 1.
|
||||||
|
|
||||||
The game is over when the head passes the wall (more than 280 pixels from the
|
The scoreboard at the top of the window reads `Score: 0 High Score: 0`. The score
|
||||||
centre on any side) or touches the tail. The snake stops, `GAME OVER` appears in
|
counts the foods eaten since the last reset; the high score is the best score so
|
||||||
the middle of the window, and a click on the window closes it. You can also close
|
far.
|
||||||
the window with its close button at any time. (Milestone 3 replaces this with
|
|
||||||
continuous play and a high score.)
|
The game does not end. When the head passes the wall (more than 280 pixels from
|
||||||
|
the centre on any side) or touches the tail, the old snake is sent off the
|
||||||
|
screen, a new snake of three segments appears in the middle, the score goes back
|
||||||
|
to 0, and the high score is raised if the score was higher. Close the window with
|
||||||
|
its close button when you have had enough. The high score is lost when the game
|
||||||
|
is closed until milestone 4 stores it in a file.
|
||||||
|
|
||||||
## Run the tests
|
## Run the tests
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
|||||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 006 |
|
| MIL | Milestone / Gateway | docs/milestones/*.md | 006 |
|
||||||
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 011 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 012 |
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# RC-011: Code review of MIL-003 (continuous play and high score)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-011 |
|
||||||
|
| CrossReference | [MIL-003], [QC-PY-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-13 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [8c81988] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: the source and tests that [MIL-003] delivers (continuous play and high score)
|
||||||
|
- Checklist used: [QC-PY-001]
|
||||||
|
- Scope: full review of the files changed on the branch of the milestone
|
||||||
|
- Language and domain: n/a
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the assistant also wrote the code)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | `Snake.reset`, `Scoreboard.reset`, `reset_if_collision`, `high_score` and `SEGMENT_PARKING_POSITION` follow PEP 8; `reset` and `high_score` are the lecture's names. |
|
||||||
|
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose; `HIGH_SCORE_LABEL` and `SEGMENT_PARKING_POSITION` say what they hold. |
|
||||||
|
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` exit 0; no `noqa`. |
|
||||||
|
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | `mypy` strict reports no issues in 13 files; every new def is annotated with `-> None` or `-> bool`. |
|
||||||
|
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No exception handling was added; the one handler in `main.main` is unchanged. |
|
||||||
|
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable default argument; no shadowed builtin. |
|
||||||
|
| 7 | Files, locks and connections are managed with context managers | N-A | No file is opened in this milestone (MIL-004). |
|
||||||
|
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | `reset`, `reset_if_collision` and the new constants have Doxygen comments; `doxygen Doxyfile` ends with 0 warnings. |
|
||||||
|
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` and no logging. |
|
||||||
|
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | No Design Class Diagram exists (deviation recorded in RC-010 and the pull request); the change adds `reset` to the base classes as the lecture does. |
|
||||||
|
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 149 tests run without a display; new tests cover higher, equal and lower score on `reset`, the parking of old segments, the new head, the forgotten direction, and `main` going on after the wall and the tail. |
|
||||||
|
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `strict = true`; `ScoreboardLike` stays a narrow protocol (`increase_score`, `reset`); no `Any`. |
|
||||||
|
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies = []` is unchanged. |
|
||||||
|
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | The label, the parking place and the other values are defined once in `constants.py`; `GAME_OVER_*` is removed, not duplicated. |
|
||||||
|
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `snake.py` and `main.py` still import no `turtle` or `tkinter` at module level (tested); no import cycle. |
|
||||||
|
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | `Snake.reset` reuses `create_snake`, so the starting positions are one piece of knowledge; `main` depends on the `ScoreboardLike` protocol. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — all Mandatory criteria pass or are not applicable. Assessed on 2026-10-13 by the assistant that wrote the code, as stand-in for S01 in autonomous mode. Mode: autonomous, not independent. The pull request is the second look.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Challenge this `Go` in the run summary or on the pull request if the stand-in decision is not wanted | S01 | 2026-10-20 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-003]: ../../milestones/mil-003-continuous-play-and-high-score.md
|
||||||
|
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||||
|
[8c81988]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/024-snake/commit/8c8198827bfc69c75a4f8ff8a393376d073e94c1
|
||||||
@@ -117,8 +117,14 @@ SCOREBOARD_FONT: Final[tuple[str, int, str]] = ("Arial", 24, "normal")
|
|||||||
## @brief The head touches the tail when it is closer to a segment than this, in pixels.
|
## @brief The head touches the tail when it is closer to a segment than this, in pixels.
|
||||||
TAIL_COLLISION_DISTANCE: Final[int] = 10
|
TAIL_COLLISION_DISTANCE: Final[int] = 10
|
||||||
|
|
||||||
## @brief Text shown when the game is over.
|
## @brief Text in front of the high score on the scoreboard.
|
||||||
GAME_OVER_TEXT: Final[str] = "GAME OVER"
|
#
|
||||||
|
# The scoreboard writes `Score: <score> High Score: <high_score>`: the score
|
||||||
|
# label, the score, a space, this label and the high score.
|
||||||
|
HIGH_SCORE_LABEL: Final[str] = "High Score: "
|
||||||
|
|
||||||
## @brief Where the game-over text is written: the centre of the screen.
|
## @brief Where a segment of the old snake is sent when the snake is reset.
|
||||||
GAME_OVER_POSITION: Final[tuple[int, int]] = (0, 0)
|
#
|
||||||
|
# The place is far outside the 600 by 600 screen, so the old segments are out
|
||||||
|
# of sight, as the day-24 lecture does it.
|
||||||
|
SEGMENT_PARKING_POSITION: Final[tuple[int, int]] = (1000, 1000)
|
||||||
|
|||||||
+16
-18
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
The flow follows the lectures "Screen Setup and Creating a Snake Body",
|
The flow follows the lectures "Screen Setup and Creating a Snake Body",
|
||||||
"Animating the Snake Segments on Screen" and "Controlling the Snake with
|
"Animating the Snake Segments on Screen" and "Controlling the Snake with
|
||||||
Keypresses", and the day-21 steps in which the snake eats food, the score rises and
|
Keypresses", the day-21 steps in which the snake eats food and the score rises,
|
||||||
the game ends.
|
and the day-24 steps in which a collision resets the game and it goes on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@@ -102,7 +102,7 @@ class FoodLike(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
class ScoreboardLike(Protocol):
|
class ScoreboardLike(Protocol):
|
||||||
"""! @brief What the eating and game-over checks need from the scoreboard.
|
"""! @brief What the eating and reset checks need from the scoreboard.
|
||||||
|
|
||||||
A `Scoreboard` fits this description, and so does a fake in a test.
|
A `Scoreboard` fits this description, and so does a fake in a test.
|
||||||
"""
|
"""
|
||||||
@@ -111,8 +111,8 @@ class ScoreboardLike(Protocol):
|
|||||||
"""! @brief Add 1 to the score and write it."""
|
"""! @brief Add 1 to the score and write it."""
|
||||||
...
|
...
|
||||||
|
|
||||||
def game_over(self) -> None:
|
def reset(self) -> None:
|
||||||
"""! @brief Write the game-over text."""
|
"""! @brief Keep the best score and start the score again at 0."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@@ -190,17 +190,19 @@ def eat_food_if_close(snake: Snake, food: FoodLike, scoreboard: ScoreboardLike)
|
|||||||
scoreboard.increase_score()
|
scoreboard.increase_score()
|
||||||
|
|
||||||
|
|
||||||
def end_game_if_over(snake: Snake, scoreboard: ScoreboardLike) -> bool:
|
def reset_if_collision(snake: Snake, scoreboard: ScoreboardLike) -> bool:
|
||||||
"""! @brief End the game when the head passes the wall or touches the tail.
|
"""! @brief Start again when the head passes the wall or touches the tail.
|
||||||
|
|
||||||
When the game is over the scoreboard writes the game-over text.
|
The game does not end: the scoreboard keeps the best score and starts the
|
||||||
|
score again, and the snake is made again at the starting positions.
|
||||||
|
|
||||||
@param snake The snake.
|
@param snake The snake.
|
||||||
@param scoreboard The scoreboard that writes the game-over text.
|
@param scoreboard The scoreboard that keeps the high score.
|
||||||
@return True when the game is over.
|
@return True when the snake and the score were reset.
|
||||||
"""
|
"""
|
||||||
if snake.hits_wall() or snake.hits_tail():
|
if snake.hits_wall() or snake.hits_tail():
|
||||||
scoreboard.game_over()
|
scoreboard.reset()
|
||||||
|
snake.reset()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -212,9 +214,8 @@ def main() -> None:
|
|||||||
`tkinter.TclError` ("invalid command name"), and `turtle` raises
|
`tkinter.TclError` ("invalid command name"), and `turtle` raises
|
||||||
`turtle.Terminator` in some other calls once its window is gone. Both mean
|
`turtle.Terminator` in some other calls once its window is gone. Both mean
|
||||||
"the player closed the window", so the game ends quietly with exit code 0.
|
"the player closed the window", so the game ends quietly with exit code 0.
|
||||||
When the snake passes the wall or touches its tail the loop ends, the screen is
|
When the snake passes the wall or touches its tail the score and the snake are
|
||||||
updated so that the game-over text shows, and `screen.exitonclick()` waits for a
|
reset and the game goes on, so the loop runs until the window is closed.
|
||||||
click.
|
|
||||||
|
|
||||||
`turtle` and `tkinter` are imported here and not at the top of the module, and
|
`turtle` and `tkinter` are imported here and not at the top of the module, and
|
||||||
so are `food` and `scoreboard`, whose classes inherit from `Turtle`: importing
|
so are `food` and `scoreboard`, whose classes inherit from `Turtle`: importing
|
||||||
@@ -240,9 +241,6 @@ def main() -> None:
|
|||||||
while game_is_on:
|
while game_is_on:
|
||||||
play_frame(screen, snake)
|
play_frame(screen, snake)
|
||||||
eat_food_if_close(snake, food, scoreboard)
|
eat_food_if_close(snake, food, scoreboard)
|
||||||
if end_game_if_over(snake, scoreboard):
|
reset_if_collision(snake, scoreboard)
|
||||||
game_is_on = False
|
|
||||||
screen.update()
|
|
||||||
screen.exitonclick()
|
|
||||||
except (Terminator, TclError):
|
except (Terminator, TclError):
|
||||||
return # the window was closed: there is nothing left to do
|
return # the window was closed: there is nothing left to do
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ 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,
|
||||||
@@ -26,8 +25,10 @@ class Scoreboard(Turtle):
|
|||||||
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 in this game.
|
## @brief The number of foods the snake has eaten since the last reset.
|
||||||
self.score = 0
|
self.score = 0
|
||||||
|
## @brief The largest score reached so far.
|
||||||
|
self.high_score = 0
|
||||||
self.color(SCOREBOARD_COLOR)
|
self.color(SCOREBOARD_COLOR)
|
||||||
self.penup()
|
self.penup()
|
||||||
self.hideturtle()
|
self.hideturtle()
|
||||||
@@ -35,10 +36,10 @@ class Scoreboard(Turtle):
|
|||||||
self.update_scoreboard()
|
self.update_scoreboard()
|
||||||
|
|
||||||
def update_scoreboard(self) -> None:
|
def update_scoreboard(self) -> None:
|
||||||
"""! @brief Wipe the old text and write the label and the score."""
|
"""! @brief Wipe the old text and write the score and the high score."""
|
||||||
self.clear()
|
self.clear()
|
||||||
self.write(
|
self.write(
|
||||||
f"{SCORE_LABEL}{self.score}",
|
f"{SCORE_LABEL}{self.score} {HIGH_SCORE_LABEL}{self.high_score}",
|
||||||
align=SCOREBOARD_ALIGNMENT,
|
align=SCOREBOARD_ALIGNMENT,
|
||||||
font=SCOREBOARD_FONT,
|
font=SCOREBOARD_FONT,
|
||||||
)
|
)
|
||||||
@@ -48,11 +49,13 @@ class Scoreboard(Turtle):
|
|||||||
self.score += 1
|
self.score += 1
|
||||||
self.update_scoreboard()
|
self.update_scoreboard()
|
||||||
|
|
||||||
def game_over(self) -> None:
|
def reset(self) -> None:
|
||||||
"""! @brief Write the game-over text at the centre of the screen.
|
"""! @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
|
When the score is higher than the high score, it becomes the new high
|
||||||
off in the game, so the screen must be updated after this call.
|
score. The score then goes back to 0.
|
||||||
"""
|
"""
|
||||||
self.goto(GAME_OVER_POSITION)
|
if self.score > self.high_score:
|
||||||
self.write(GAME_OVER_TEXT, align=SCOREBOARD_ALIGNMENT, font=SCOREBOARD_FONT)
|
self.high_score = self.score
|
||||||
|
self.score = 0
|
||||||
|
self.update_scoreboard()
|
||||||
|
|||||||
+18
-2
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
The class follows the lectures "Create a Snake Class & Move to OOP", "Animating
|
The class follows the lectures "Create a Snake Class & Move to OOP", "Animating
|
||||||
the Snake Segments on Screen" and "Controlling the Snake with Keypresses", and
|
the Snake Segments on Screen" and "Controlling the Snake with Keypresses", and
|
||||||
the day-21 steps in which the snake grows and the game ends. It keeps the lectures'
|
the day-21 steps in which the snake grows, and the day-24 step in which it is reset
|
||||||
|
after a collision. It keeps the lectures'
|
||||||
names: `Snake`, `segments`, `create_snake`, `add_segment`, `extend`, `move`, `head`,
|
names: `Snake`, `segments`, `create_snake`, `add_segment`, `extend`, `move`, `head`,
|
||||||
`up`, `down`, `left` and `right`. The only differences are that the snake gets
|
`reset`, `up`, `down`, `left` and `right`. The only differences are that the snake gets
|
||||||
the function that makes a segment as a parameter, so that a test can pass a fake
|
the function that makes a segment as a parameter, so that a test can pass a fake
|
||||||
and needs no window, and how a reversal is refused (see Snake).
|
and needs no window, and how a reversal is refused (see Snake).
|
||||||
"""
|
"""
|
||||||
@@ -20,6 +21,7 @@ from snake_game.constants import (
|
|||||||
LEFT,
|
LEFT,
|
||||||
MOVE_DISTANCE,
|
MOVE_DISTANCE,
|
||||||
RIGHT,
|
RIGHT,
|
||||||
|
SEGMENT_PARKING_POSITION,
|
||||||
SEGMENT_SHAPE,
|
SEGMENT_SHAPE,
|
||||||
STARTING_POSITIONS,
|
STARTING_POSITIONS,
|
||||||
TAIL_COLLISION_DISTANCE,
|
TAIL_COLLISION_DISTANCE,
|
||||||
@@ -204,6 +206,20 @@ class Snake:
|
|||||||
self.head.forward(MOVE_DISTANCE)
|
self.head.forward(MOVE_DISTANCE)
|
||||||
self._direction_of_travel = self.head.heading()
|
self._direction_of_travel = self.head.heading()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""! @brief Start again with a new snake of three segments.
|
||||||
|
|
||||||
|
Every old segment is sent far outside the screen, so it can no longer be
|
||||||
|
seen, the list is emptied, and the snake is created again at the starting
|
||||||
|
positions. The new head is the first new segment, and it points right.
|
||||||
|
"""
|
||||||
|
for segment in self.segments:
|
||||||
|
segment.goto(*SEGMENT_PARKING_POSITION)
|
||||||
|
self.segments.clear()
|
||||||
|
self.create_snake()
|
||||||
|
self.head = self.segments[0]
|
||||||
|
self._direction_of_travel = self.head.heading()
|
||||||
|
|
||||||
def hits_wall(self) -> bool:
|
def hits_wall(self) -> bool:
|
||||||
"""! @brief Tell whether the head has passed the wall on any side.
|
"""! @brief Tell whether the head has passed the wall on any side.
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -138,13 +138,13 @@ class FakeScoreboard:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.increases = 0
|
self.increases = 0
|
||||||
self.game_overs = 0
|
self.resets = 0
|
||||||
|
|
||||||
def increase_score(self) -> None:
|
def increase_score(self) -> None:
|
||||||
self.increases += 1
|
self.increases += 1
|
||||||
|
|
||||||
def game_over(self) -> None:
|
def reset(self) -> None:
|
||||||
self.game_overs += 1
|
self.resets += 1
|
||||||
|
|
||||||
|
|
||||||
class FakeScreen:
|
class FakeScreen:
|
||||||
|
|||||||
+14
-6
@@ -154,10 +154,18 @@ def test_tail_is_touched_closer_than_10_pixels_which_is_less_than_a_move() -> No
|
|||||||
assert 0 < constants.TAIL_COLLISION_DISTANCE < constants.MOVE_DISTANCE
|
assert 0 < constants.TAIL_COLLISION_DISTANCE < constants.MOVE_DISTANCE
|
||||||
|
|
||||||
|
|
||||||
def test_game_over_text_is_written_at_the_centre_inside_the_wall() -> None:
|
def test_game_over_constants_no_longer_exist() -> None:
|
||||||
x, y = constants.GAME_OVER_POSITION
|
assert not hasattr(constants, "GAME_OVER_TEXT")
|
||||||
|
assert not hasattr(constants, "GAME_OVER_POSITION")
|
||||||
|
|
||||||
assert constants.GAME_OVER_TEXT == "GAME OVER"
|
|
||||||
assert (x, y) == (0, 0)
|
def test_high_score_label_follows_the_score_label_as_in_the_lecture() -> None:
|
||||||
assert abs(x) < constants.WALL_LIMIT
|
assert constants.HIGH_SCORE_LABEL == "High Score: "
|
||||||
assert abs(y) < constants.WALL_LIMIT
|
assert constants.HIGH_SCORE_LABEL != constants.SCORE_LABEL
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_segments_are_parked_outside_the_screen() -> None:
|
||||||
|
x, y = constants.SEGMENT_PARKING_POSITION
|
||||||
|
|
||||||
|
assert abs(x) > HALF_WIDTH
|
||||||
|
assert abs(y) > HALF_HEIGHT
|
||||||
|
|||||||
+52
-45
@@ -20,10 +20,10 @@ from snake_game.main import (
|
|||||||
bind_keys,
|
bind_keys,
|
||||||
configure_screen,
|
configure_screen,
|
||||||
eat_food_if_close,
|
eat_food_if_close,
|
||||||
end_game_if_over,
|
|
||||||
main,
|
main,
|
||||||
play_frame,
|
play_frame,
|
||||||
register_head_shape,
|
register_head_shape,
|
||||||
|
reset_if_collision,
|
||||||
)
|
)
|
||||||
from snake_game.snake import Snake
|
from snake_game.snake import Snake
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ def test_main_lets_the_snake_eat_the_food_that_lies_on_its_way(
|
|||||||
("goto", (20, 0)),
|
("goto", (20, 0)),
|
||||||
("goto", (constants.WALL_LIMIT, constants.WALL_LIMIT)),
|
("goto", (constants.WALL_LIMIT, constants.WALL_LIMIT)),
|
||||||
]
|
]
|
||||||
assert scoreboard.calls[-1][1][0] == "Score: 1"
|
assert scoreboard.calls[-1][1][0] == "Score: 1 High Score: 0"
|
||||||
assert new_segment.position() == (-20, 0) # where the last segment was
|
assert new_segment.position() == (-20, 0) # where the last segment was
|
||||||
|
|
||||||
|
|
||||||
@@ -288,86 +288,93 @@ def test_main_does_not_raise_the_score_when_the_food_is_far_away(
|
|||||||
main()
|
main()
|
||||||
|
|
||||||
scoreboard = turtles[4]
|
scoreboard = turtles[4]
|
||||||
assert scoreboard.calls[-1][1][0] == "Score: 0"
|
assert scoreboard.calls[-1][1][0] == "Score: 0 High Score: 0"
|
||||||
assert len(turtles) == len(constants.STARTING_POSITIONS) + 2
|
assert len(turtles) == len(constants.STARTING_POSITIONS) + 2
|
||||||
|
|
||||||
|
|
||||||
def test_the_game_is_not_over_while_the_snake_is_inside_and_clear_of_its_tail() -> None:
|
def test_nothing_is_reset_while_the_snake_is_inside_and_clear_of_its_tail() -> None:
|
||||||
snake, _ = make_snake()
|
snake, created = make_snake()
|
||||||
scoreboard = FakeScoreboard()
|
scoreboard = FakeScoreboard()
|
||||||
|
|
||||||
assert not end_game_if_over(snake, scoreboard)
|
assert not reset_if_collision(snake, scoreboard)
|
||||||
assert scoreboard.game_overs == 0
|
assert scoreboard.resets == 0
|
||||||
|
assert len(created) == len(constants.STARTING_POSITIONS)
|
||||||
|
|
||||||
|
|
||||||
def test_the_game_is_over_when_the_head_passes_the_wall() -> None:
|
def test_the_game_is_reset_when_the_head_passes_the_wall() -> None:
|
||||||
snake, _ = make_snake()
|
snake, created = make_snake()
|
||||||
snake.head.goto(constants.WALL_LIMIT + 1, 0)
|
snake.head.goto(constants.WALL_LIMIT + 1, 0)
|
||||||
scoreboard = FakeScoreboard()
|
scoreboard = FakeScoreboard()
|
||||||
|
|
||||||
assert end_game_if_over(snake, scoreboard)
|
assert reset_if_collision(snake, scoreboard)
|
||||||
assert scoreboard.game_overs == 1
|
assert scoreboard.resets == 1
|
||||||
|
assert len(created) == 2 * len(constants.STARTING_POSITIONS)
|
||||||
|
|
||||||
|
|
||||||
def test_the_game_is_over_when_the_head_touches_the_tail() -> None:
|
def test_the_game_is_reset_when_the_head_touches_the_tail() -> None:
|
||||||
snake, created = make_snake()
|
snake, created = make_snake()
|
||||||
created[1].goto(3, 4)
|
created[1].goto(3, 4)
|
||||||
scoreboard = FakeScoreboard()
|
scoreboard = FakeScoreboard()
|
||||||
|
|
||||||
assert end_game_if_over(snake, scoreboard)
|
assert reset_if_collision(snake, scoreboard)
|
||||||
assert scoreboard.game_overs == 1
|
assert scoreboard.resets == 1
|
||||||
|
assert snake.head is not created[0]
|
||||||
|
|
||||||
|
|
||||||
def test_main_ends_the_game_at_the_wall_shows_game_over_and_waits_for_a_click(
|
def test_main_goes_on_after_the_wall_with_a_new_snake_and_no_game_over(
|
||||||
monkeypatch: pytest.MonkeyPatch, sleeps: list[float]
|
monkeypatch: pytest.MonkeyPatch, sleeps: list[float]
|
||||||
) -> None:
|
) -> None:
|
||||||
turtles, screens = install_fake_turtle(monkeypatch, frames_before_close=100)
|
moves_to_pass_the_wall = constants.WALL_LIMIT // constants.MOVE_DISTANCE + 1
|
||||||
|
turtles, screens = install_fake_turtle(
|
||||||
|
monkeypatch, frames_before_close=moves_to_pass_the_wall + 2
|
||||||
|
)
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
snake_head, scoreboard = turtles[0], turtles[4]
|
old_head, scoreboard = turtles[0], turtles[4]
|
||||||
moves_to_pass_the_wall = constants.WALL_LIMIT // constants.MOVE_DISTANCE + 1
|
new_head = turtles[5]
|
||||||
assert snake_head.call_names().count("forward") == moves_to_pass_the_wall
|
written = [call[1][0] for call in scoreboard.calls if call[0] == "write"]
|
||||||
assert snake_head.position() == (
|
assert old_head.position() == constants.SEGMENT_PARKING_POSITION
|
||||||
constants.MOVE_DISTANCE * moves_to_pass_the_wall,
|
assert new_head.call_names().count("forward") == 2
|
||||||
0,
|
assert new_head.position() == (2 * constants.MOVE_DISTANCE, 0)
|
||||||
)
|
|
||||||
assert scoreboard.calls[-1] == (
|
assert scoreboard.calls[-1] == (
|
||||||
"write",
|
"write",
|
||||||
("GAME OVER", "center", ("Arial", 24, "normal")),
|
("Score: 0 High Score: 0", "center", ("Arial", 24, "normal")),
|
||||||
)
|
)
|
||||||
assert screens[0].call_names()[-2:] == ["update", "exitonclick"]
|
assert "GAME OVER" not in written
|
||||||
assert screens[0].call_names().count("exitonclick") == 1
|
assert "exitonclick" not in screens[0].call_names()
|
||||||
assert sleeps == [constants.REFRESH_DELAY_SECONDS] * moves_to_pass_the_wall
|
|
||||||
|
|
||||||
|
|
||||||
def test_main_ends_the_game_when_the_snake_touches_its_tail(
|
def test_main_shows_the_best_score_after_a_reset(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, sleeps: list[float]
|
||||||
|
) -> None:
|
||||||
|
script_randint(monkeypatch, [20, 0]) # the first food lies one move ahead
|
||||||
|
moves_to_pass_the_wall = constants.WALL_LIMIT // constants.MOVE_DISTANCE + 1
|
||||||
|
turtles, _ = install_fake_turtle(
|
||||||
|
monkeypatch, frames_before_close=moves_to_pass_the_wall
|
||||||
|
)
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
scoreboard = turtles[4]
|
||||||
|
assert scoreboard.calls[-1][1][0] == "Score: 0 High Score: 1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_resets_when_the_snake_touches_its_tail(
|
||||||
monkeypatch: pytest.MonkeyPatch, sleeps: list[float]
|
monkeypatch: pytest.MonkeyPatch, sleeps: list[float]
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setattr("snake_game.snake.Snake.hits_tail", lambda self: True)
|
monkeypatch.setattr("snake_game.snake.Snake.hits_tail", lambda self: True)
|
||||||
turtles, screens = install_fake_turtle(monkeypatch, frames_before_close=100)
|
turtles, screens = install_fake_turtle(monkeypatch, frames_before_close=2)
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
assert turtles[0].call_names().count("forward") == 1
|
assert turtles[0].call_names().count("forward") == 1
|
||||||
assert turtles[4].calls[-1][1][0] == "GAME OVER"
|
assert turtles[0].position() == constants.SEGMENT_PARKING_POSITION
|
||||||
assert screens[0].call_names()[-1] == "exitonclick"
|
assert "exitonclick" not in screens[0].call_names()
|
||||||
|
|
||||||
|
|
||||||
def test_main_does_not_move_the_snake_after_game_over(
|
|
||||||
monkeypatch: pytest.MonkeyPatch, sleeps: list[float]
|
|
||||||
) -> None:
|
|
||||||
turtles, screens = install_fake_turtle(monkeypatch, frames_before_close=100)
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
||||||
updates = screens[0].call_names().count("update")
|
|
||||||
moves = turtles[0].call_names().count("forward")
|
|
||||||
assert updates == moves + 1 # one update per frame, and one for the last text
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("closing_error", [FakeTerminatorError, FakeTclError])
|
@pytest.mark.parametrize("closing_error", [FakeTerminatorError, FakeTclError])
|
||||||
def test_main_ends_quietly_when_the_window_is_closed_after_game_over(
|
def test_main_ends_quietly_when_the_window_is_closed_after_a_reset(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
sleeps: list[float],
|
sleeps: list[float],
|
||||||
closing_error: type[Exception],
|
closing_error: type[Exception],
|
||||||
@@ -381,7 +388,7 @@ def test_main_ends_quietly_when_the_window_is_closed_after_game_over(
|
|||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
assert turtles[4].calls[-1][1][0] == "GAME OVER"
|
assert turtles[4].calls[-1][1][0] == "Score: 0 High Score: 0"
|
||||||
assert "exitonclick" not in screens[0].call_names()
|
assert "exitonclick" not in screens[0].call_names()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+66
-14
@@ -8,7 +8,10 @@ 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(
|
def test_scoreboard_is_white_hidden_and_at_the_top_centre(
|
||||||
@@ -29,7 +32,7 @@ def test_scoreboard_inherits_from_turtle(monkeypatch: pytest.MonkeyPatch) -> Non
|
|||||||
assert issubclass(module.Scoreboard, sys.modules["turtle"].Turtle)
|
assert issubclass(module.Scoreboard, sys.modules["turtle"].Turtle)
|
||||||
|
|
||||||
|
|
||||||
def test_scoreboard_starts_by_writing_score_0_centred_in_arial_24(
|
def test_scoreboard_starts_by_writing_both_scores_0_centred_in_arial_24(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
scoreboard = make_scoreboard(monkeypatch)
|
scoreboard = make_scoreboard(monkeypatch)
|
||||||
@@ -47,7 +50,7 @@ def test_increase_score_wipes_the_old_text_and_writes_the_new_score(
|
|||||||
assert scoreboard.call_names()[-2:] == ["clear", "write"]
|
assert scoreboard.call_names()[-2:] == ["clear", "write"]
|
||||||
assert scoreboard.calls[-1] == (
|
assert scoreboard.calls[-1] == (
|
||||||
"write",
|
"write",
|
||||||
("Score: 1", "center", ("Arial", 24, "normal")),
|
("Score: 1 High Score: 0", "center", ("Arial", 24, "normal")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,29 +60,78 @@ def test_every_food_adds_one_to_the_score(monkeypatch: pytest.MonkeyPatch) -> No
|
|||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
scoreboard.increase_score()
|
scoreboard.increase_score()
|
||||||
|
|
||||||
assert scoreboard.calls[-1][1][0] == "Score: 3"
|
assert scoreboard.calls[-1][1][0] == "Score: 3 High Score: 0"
|
||||||
|
|
||||||
|
|
||||||
def test_game_over_writes_the_text_at_the_centre_in_the_scoreboard_font(
|
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,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
scoreboard = make_scoreboard(monkeypatch)
|
scoreboard = make_scoreboard(monkeypatch)
|
||||||
|
for _ in range(3):
|
||||||
|
scoreboard.increase_score()
|
||||||
|
|
||||||
scoreboard.game_over()
|
scoreboard.reset()
|
||||||
|
|
||||||
assert scoreboard.calls[-2:] == [
|
assert scoreboard.high_score == 3
|
||||||
("goto", constants.GAME_OVER_POSITION),
|
assert scoreboard.score == 0
|
||||||
("write", ("GAME OVER", "center", ("Arial", 24, "normal"))),
|
assert scoreboard.call_names()[-2:] == ["clear", "write"]
|
||||||
]
|
assert scoreboard.calls[-1] == (
|
||||||
|
"write",
|
||||||
|
("Score: 0 High Score: 3", "center", ("Arial", 24, "normal")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_game_over_leaves_the_score_on_the_screen(
|
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,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
scoreboard = make_scoreboard(monkeypatch)
|
scoreboard = make_scoreboard(monkeypatch)
|
||||||
scoreboard.increase_score()
|
scoreboard.increase_score()
|
||||||
calls_before = len(scoreboard.calls)
|
scoreboard.reset()
|
||||||
|
scoreboard.increase_score()
|
||||||
|
|
||||||
scoreboard.game_over()
|
scoreboard.reset()
|
||||||
|
|
||||||
assert "clear" not in scoreboard.call_names()[calls_before:]
|
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")
|
||||||
|
|||||||
@@ -407,3 +407,61 @@ def test_a_snake_of_four_segments_cannot_touch_its_tail_in_a_small_square() -> N
|
|||||||
snake.move()
|
snake.move()
|
||||||
|
|
||||||
assert not snake.hits_tail()
|
assert not snake.hits_tail()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_sends_every_old_segment_off_the_screen() -> None:
|
||||||
|
snake, created = make_snake()
|
||||||
|
snake.extend()
|
||||||
|
old = list(created)
|
||||||
|
|
||||||
|
snake.reset()
|
||||||
|
|
||||||
|
assert len(old) == len(constants.STARTING_POSITIONS) + 1
|
||||||
|
assert all(
|
||||||
|
segment.position() == constants.SEGMENT_PARKING_POSITION for segment in old
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_makes_the_snake_again_at_the_starting_positions() -> None:
|
||||||
|
snake, created = make_snake()
|
||||||
|
snake.extend()
|
||||||
|
snake.extend()
|
||||||
|
old_count = len(created)
|
||||||
|
|
||||||
|
snake.reset()
|
||||||
|
|
||||||
|
assert len(snake.segments) == len(constants.STARTING_POSITIONS)
|
||||||
|
assert positions(snake.segments) == [
|
||||||
|
(float(x), float(y)) for x, y in constants.STARTING_POSITIONS
|
||||||
|
]
|
||||||
|
assert snake.segments == created[old_count:]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_makes_the_first_new_segment_the_head() -> None:
|
||||||
|
snake, created = make_snake()
|
||||||
|
old_head = snake.head
|
||||||
|
|
||||||
|
snake.reset()
|
||||||
|
|
||||||
|
new_head = created[len(constants.STARTING_POSITIONS)]
|
||||||
|
assert snake.head is snake.segments[0] is new_head
|
||||||
|
assert snake.head is not old_head
|
||||||
|
assert ("shape", (constants.HEAD_SHAPE,)) in new_head.calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_snake_moves_right_from_the_starting_positions() -> None:
|
||||||
|
snake, _ = make_snake_moving(constants.UP)
|
||||||
|
|
||||||
|
snake.reset()
|
||||||
|
snake.move()
|
||||||
|
|
||||||
|
assert snake.head.position() == (constants.MOVE_DISTANCE, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_forgets_the_direction_of_the_last_move() -> None:
|
||||||
|
snake, _ = make_snake_moving(constants.DOWN)
|
||||||
|
|
||||||
|
snake.reset()
|
||||||
|
snake.up() # allowed again: the new snake moves right, not down
|
||||||
|
|
||||||
|
assert snake.head.heading() == constants.UP
|
||||||
|
|||||||
Reference in New Issue
Block a user