From ab92d5871ce33788ad41ee2fa4307a4a9ca90110 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Thu, 8 Oct 2026 18:31:04 +0800 Subject: [PATCH] Adopt the Food class and its tests Copied unchanged from the base: class Food(Turtle) with refresh, and one random_coordinate function that the tests replace. MIL-003 checks the values against the day-21 lectures. Task: MIL-002#3 Closes #10 --- src/snake_game/food.py | 49 +++++++++++++++++++++++++++ tests/test_food.py | 77 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/snake_game/food.py create mode 100644 tests/test_food.py diff --git a/src/snake_game/food.py b/src/snake_game/food.py new file mode 100644 index 0000000..d88225e --- /dev/null +++ b/src/snake_game/food.py @@ -0,0 +1,49 @@ +"""! @file +@brief The food: a small dot that the snake eats. + +The class follows the day-21 lecture on inheritance: `Food` is a `Turtle` that +sets itself up and moves to a random place. Because it inherits from `Turtle`, +this module needs `turtle` (and so `tkinter`) when it is imported; the game +imports it only when it starts, and the tests install a fake `turtle` module +first. +""" + +import random +from turtle import Turtle + +from snake_game.constants import ( + FOOD_COLOR, + FOOD_SHAPE, + FOOD_SIZE, + FOOD_SPEED, + WALL_LIMIT, +) + + +def random_coordinate() -> int: + """! @brief Pick a random x or y inside the walls. + + All the randomness of the food comes from this one function, so that a test can + replace it. + + @return A whole number from `-WALL_LIMIT` to `WALL_LIMIT`. + """ + return random.randint(-WALL_LIMIT, WALL_LIMIT) + + +class Food(Turtle): + """! @brief The food, a blue circle at a random place inside the walls.""" + + def __init__(self) -> None: + """! @brief Set the food up and put it at a random place.""" + super().__init__() + self.shape(FOOD_SHAPE) + self.penup() + self.shapesize(stretch_wid=FOOD_SIZE, stretch_len=FOOD_SIZE) + self.color(FOOD_COLOR) + self.speed(FOOD_SPEED) + self.refresh() + + def refresh(self) -> None: + """! @brief Move the food to a new random place inside the walls.""" + self.goto(random_coordinate(), random_coordinate()) diff --git a/tests/test_food.py b/tests/test_food.py new file mode 100644 index 0000000..34a6cbb --- /dev/null +++ b/tests/test_food.py @@ -0,0 +1,77 @@ +"""Tests of the food, with a fake `Turtle` as its base class.""" + +import importlib +import sys + +import pytest + +from fakes import install_fake_turtle, make_food, script_randint +from snake_game import constants + + +def test_food_is_a_blue_half_size_circle_with_the_pen_up_and_no_animation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + food = make_food(monkeypatch) + + assert ("shape", ("circle",)) in food.calls + assert ("shapesize", (0.5, 0.5)) in food.calls + assert ("color", ("blue",)) in food.calls + assert ("speed", ("fastest",)) in food.calls + assert "penup" in food.call_names() + + +def test_food_inherits_from_turtle_and_calls_the_base_class_first( + monkeypatch: pytest.MonkeyPatch, +) -> None: + install_fake_turtle(monkeypatch) + module = importlib.import_module("snake_game.food") + + food = module.Food() + + assert issubclass(module.Food, sys.modules["turtle"].Turtle) + assert food.calls[0][0] == "shape" # the base class set up `calls` before + + +def test_food_starts_at_a_random_place(monkeypatch: pytest.MonkeyPatch) -> None: + script_randint(monkeypatch, [12, -34]) + + food = make_food(monkeypatch) + + assert food.position() == (12, -34) + + +def test_refresh_moves_the_food_to_a_new_random_place( + monkeypatch: pytest.MonkeyPatch, +) -> None: + script_randint(monkeypatch, [1, 2, 3, 4]) + food = make_food(monkeypatch) + + food.refresh() + + assert food.position() == (3, 4) + + +def test_random_coordinate_asks_for_a_number_from_wall_to_wall( + monkeypatch: pytest.MonkeyPatch, +) -> None: + bounds = script_randint(monkeypatch, [7]) + install_fake_turtle(monkeypatch) + module = importlib.import_module("snake_game.food") + + result = module.random_coordinate() + + assert result == 7 + assert bounds == [(-constants.WALL_LIMIT, constants.WALL_LIMIT)] + + +def test_random_places_are_always_inside_the_walls( + monkeypatch: pytest.MonkeyPatch, +) -> None: + install_fake_turtle(monkeypatch) + module = importlib.import_module("snake_game.food") + + places = [module.random_coordinate() for _ in range(500)] + + wall = constants.WALL_LIMIT + assert all(-wall <= place <= wall for place in places)