+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
|
||||||
|
|||||||
+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:
|
||||||
|
|||||||
+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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user