MIL-001: Project foundation #35
@@ -0,0 +1,14 @@
|
|||||||
|
## @package snake_game
|
||||||
|
# @brief Snake game from Udemy's 100 Days of Code (day 24).
|
||||||
|
#
|
||||||
|
# @mainpage Snake Game
|
||||||
|
#
|
||||||
|
# A snake of three square segments moves across a Python turtle screen by
|
||||||
|
# itself and is steered with the arrow keys. It eats food, grows and keeps a
|
||||||
|
# score. When the head passes the wall or touches the tail the snake and the
|
||||||
|
# score are reset and the game goes on; the high score is kept in the file
|
||||||
|
# data.txt between games. The game uses the standard library only.
|
||||||
|
#
|
||||||
|
# The constants of the game are in constants.py.
|
||||||
|
|
||||||
|
"""Snake game from Udemy's 100 Days of Code (day 24)."""
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
## @file constants.py
|
||||||
|
# @brief Constants of the snake game.
|
||||||
|
#
|
||||||
|
# Every value that the day-20, day-21 and day-24 lectures fix lives here, so
|
||||||
|
# that no other module contains a magic number. The turtle coordinate system
|
||||||
|
# has its centre at (0, 0); the 600 by 600 screen therefore reaches from -300
|
||||||
|
# to 300 on both axes. A direction is a turtle heading in degrees: 0 points
|
||||||
|
# right and the angle grows counter-clockwise.
|
||||||
|
|
||||||
|
"""Constants of the snake game, as fixed by the day-20, day-21 and day-24 lectures."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
## @brief Width of the screen in pixels.
|
||||||
|
SCREEN_WIDTH: Final[int] = 600
|
||||||
|
|
||||||
|
## @brief Height of the screen in pixels.
|
||||||
|
SCREEN_HEIGHT: Final[int] = 600
|
||||||
|
|
||||||
|
## @brief Background colour of the screen.
|
||||||
|
SCREEN_BACKGROUND_COLOR: Final[str] = "black"
|
||||||
|
|
||||||
|
## @brief Title of the game window.
|
||||||
|
SCREEN_TITLE: Final[str] = "My Snake Game"
|
||||||
|
|
||||||
|
## @brief Shape of every body segment of the snake; the head has `HEAD_SHAPE`.
|
||||||
|
SEGMENT_SHAPE: Final[str] = "square"
|
||||||
|
|
||||||
|
## @brief Colour of every body segment, that is every segment behind the head.
|
||||||
|
BODY_COLOR: Final[str] = "light green"
|
||||||
|
|
||||||
|
## @brief Colour of the head of the snake.
|
||||||
|
HEAD_COLOR: Final[str] = "dark green"
|
||||||
|
|
||||||
|
## @brief Name under which the shape of the head is registered with the screen.
|
||||||
|
HEAD_SHAPE: Final[str] = "snake_head"
|
||||||
|
|
||||||
|
## @brief Outline of the head: a 20 by 20 pixel square, as the corners (x, y).
|
||||||
|
#
|
||||||
|
# These are turtle shape coordinates: the front of the shape is the positive y
|
||||||
|
# direction, which a turtle with heading 0 draws pointing right.
|
||||||
|
HEAD_OUTLINE: Final[tuple[tuple[int, int], ...]] = (
|
||||||
|
(-10, -10),
|
||||||
|
(-10, 10),
|
||||||
|
(10, 10),
|
||||||
|
(10, -10),
|
||||||
|
)
|
||||||
|
|
||||||
|
## @brief Outline of the mouth: a wedge cut into the middle of the front edge.
|
||||||
|
#
|
||||||
|
# It is 12 pixels wide at the edge and 8 pixels deep, in the same shape
|
||||||
|
# coordinates as `HEAD_OUTLINE`.
|
||||||
|
MOUTH_OUTLINE: Final[tuple[tuple[int, int], ...]] = ((-6, 10), (0, 2), (6, 10))
|
||||||
|
|
||||||
|
## @brief Colour of the mouth: the background colour, so that it looks cut out.
|
||||||
|
MOUTH_COLOR: Final[str] = "black"
|
||||||
|
|
||||||
|
## @brief Where the segments are drawn when the game starts, from head to tail.
|
||||||
|
#
|
||||||
|
# The positions lie on one row, 20 pixels apart, which is the width of a
|
||||||
|
# default turtle square, so the segments touch without overlapping.
|
||||||
|
STARTING_POSITIONS: Final[tuple[tuple[int, int], ...]] = ((0, 0), (-20, 0), (-40, 0))
|
||||||
|
|
||||||
|
## @brief Distance in pixels that the head moves in one move.
|
||||||
|
MOVE_DISTANCE: Final[int] = 20
|
||||||
|
|
||||||
|
## @brief Direction up, as a turtle heading in degrees.
|
||||||
|
UP: Final[int] = 90
|
||||||
|
|
||||||
|
## @brief Direction down, as a turtle heading in degrees.
|
||||||
|
DOWN: Final[int] = 270
|
||||||
|
|
||||||
|
## @brief Direction left, as a turtle heading in degrees.
|
||||||
|
LEFT: Final[int] = 180
|
||||||
|
|
||||||
|
## @brief Direction right, as a turtle heading in degrees.
|
||||||
|
RIGHT: Final[int] = 0
|
||||||
|
|
||||||
|
## @brief Wait in seconds between two moves.
|
||||||
|
REFRESH_DELAY_SECONDS: Final[float] = 0.1
|
||||||
|
|
||||||
|
## @brief Shape of the food.
|
||||||
|
FOOD_SHAPE: Final[str] = "circle"
|
||||||
|
|
||||||
|
## @brief Stretch factor of the food in both directions; 0.5 is half a default turtle.
|
||||||
|
FOOD_SIZE: Final[float] = 0.5
|
||||||
|
|
||||||
|
## @brief Colour of the food.
|
||||||
|
FOOD_COLOR: Final[str] = "blue"
|
||||||
|
|
||||||
|
## @brief Speed of the food; `fastest` switches its animation off.
|
||||||
|
FOOD_SPEED: Final[str] = "fastest"
|
||||||
|
|
||||||
|
## @brief How far from the centre the wall is, on every side, in pixels.
|
||||||
|
#
|
||||||
|
# The head must stay inside it, and the food is shown inside it.
|
||||||
|
WALL_LIMIT: Final[int] = 280
|
||||||
|
|
||||||
|
## @brief The snake eats the food when the head is closer to it than this, in pixels.
|
||||||
|
FOOD_COLLISION_DISTANCE: Final[int] = 15
|
||||||
|
|
||||||
|
## @brief Text in front of the score on the scoreboard.
|
||||||
|
SCORE_LABEL: Final[str] = "Score: "
|
||||||
|
|
||||||
|
## @brief Colour of the scoreboard text.
|
||||||
|
SCOREBOARD_COLOR: Final[str] = "white"
|
||||||
|
|
||||||
|
## @brief Where the scoreboard text is written: the top centre of the screen.
|
||||||
|
SCOREBOARD_POSITION: Final[tuple[int, int]] = (0, 270)
|
||||||
|
|
||||||
|
## @brief Alignment of the scoreboard text around its position.
|
||||||
|
SCOREBOARD_ALIGNMENT: Final[str] = "center"
|
||||||
|
|
||||||
|
## @brief Font of the scoreboard text: family, size and style.
|
||||||
|
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.
|
||||||
|
TAIL_COLLISION_DISTANCE: Final[int] = 10
|
||||||
|
|
||||||
|
## @brief Text shown when the game is over.
|
||||||
|
GAME_OVER_TEXT: Final[str] = "GAME OVER"
|
||||||
|
|
||||||
|
## @brief Where the game-over text is written: the centre of the screen.
|
||||||
|
GAME_OVER_POSITION: Final[tuple[int, int]] = (0, 0)
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""Tests of the constants that the day-20, day-21 and day-24 lectures fix."""
|
||||||
|
|
||||||
|
from snake_game import constants
|
||||||
|
|
||||||
|
HALF_WIDTH = constants.SCREEN_WIDTH / 2
|
||||||
|
HALF_HEIGHT = constants.SCREEN_HEIGHT / 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_screen_size_matches_lecture() -> None:
|
||||||
|
assert (constants.SCREEN_WIDTH, constants.SCREEN_HEIGHT) == (600, 600)
|
||||||
|
|
||||||
|
|
||||||
|
def test_screen_is_black_and_titled_as_in_the_lecture() -> None:
|
||||||
|
assert constants.SCREEN_BACKGROUND_COLOR == "black"
|
||||||
|
assert constants.SCREEN_TITLE == "My Snake Game"
|
||||||
|
|
||||||
|
|
||||||
|
def test_body_segments_are_light_green_squares() -> None:
|
||||||
|
assert constants.SEGMENT_SHAPE == "square"
|
||||||
|
assert constants.BODY_COLOR == "light green"
|
||||||
|
|
||||||
|
|
||||||
|
def test_segment_color_no_longer_exists() -> None:
|
||||||
|
assert not hasattr(constants, "SEGMENT_COLOR")
|
||||||
|
|
||||||
|
|
||||||
|
def test_head_is_dark_green_and_differs_from_the_body() -> None:
|
||||||
|
assert constants.HEAD_COLOR == "dark green"
|
||||||
|
assert constants.HEAD_COLOR != constants.BODY_COLOR
|
||||||
|
|
||||||
|
|
||||||
|
def test_head_shape_has_a_name_that_is_not_a_built_in_shape() -> None:
|
||||||
|
assert constants.HEAD_SHAPE == "snake_head"
|
||||||
|
assert constants.HEAD_SHAPE != constants.SEGMENT_SHAPE
|
||||||
|
|
||||||
|
|
||||||
|
def test_head_is_the_20_by_20_square() -> None:
|
||||||
|
assert constants.HEAD_OUTLINE == ((-10, -10), (-10, 10), (10, 10), (10, -10))
|
||||||
|
xs = {x for x, _ in constants.HEAD_OUTLINE}
|
||||||
|
ys = {y for _, y in constants.HEAD_OUTLINE}
|
||||||
|
assert max(xs) - min(xs) == max(ys) - min(ys) == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_mouth_lies_on_the_front_edge_and_inside_the_head() -> None:
|
||||||
|
front = max(y for _, y in constants.HEAD_OUTLINE)
|
||||||
|
half = max(x for x, _ in constants.HEAD_OUTLINE)
|
||||||
|
|
||||||
|
assert constants.MOUTH_OUTLINE == ((-6, 10), (0, 2), (6, 10))
|
||||||
|
assert any(y == front for _, y in constants.MOUTH_OUTLINE)
|
||||||
|
assert all(
|
||||||
|
abs(x) <= half and -half <= y <= front for x, y in constants.MOUTH_OUTLINE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mouth_is_the_colour_of_the_background() -> None:
|
||||||
|
assert constants.MOUTH_COLOR == constants.SCREEN_BACKGROUND_COLOR
|
||||||
|
|
||||||
|
|
||||||
|
def test_snake_starts_with_three_segments_at_the_lecture_positions() -> None:
|
||||||
|
assert constants.STARTING_POSITIONS == ((0, 0), (-20, 0), (-40, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_starting_positions_are_distinct_on_one_row() -> None:
|
||||||
|
positions = constants.STARTING_POSITIONS
|
||||||
|
|
||||||
|
assert len(set(positions)) == len(positions)
|
||||||
|
assert len({y for _, y in positions}) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_starting_positions_run_from_head_to_tail_leftwards() -> None:
|
||||||
|
x_values = [x for x, _ in constants.STARTING_POSITIONS]
|
||||||
|
|
||||||
|
assert x_values == sorted(x_values, reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_starting_positions_are_one_move_distance_apart() -> None:
|
||||||
|
x_values = [x for x, _ in constants.STARTING_POSITIONS]
|
||||||
|
gaps = {left - right for left, right in zip(x_values, x_values[1:], strict=False)}
|
||||||
|
|
||||||
|
assert gaps == {constants.MOVE_DISTANCE}
|
||||||
|
|
||||||
|
|
||||||
|
def test_starting_positions_are_inside_the_screen() -> None:
|
||||||
|
assert all(
|
||||||
|
abs(x) < HALF_WIDTH and abs(y) < HALF_HEIGHT
|
||||||
|
for x, y in constants.STARTING_POSITIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_distance_is_twenty_pixels() -> None:
|
||||||
|
assert constants.MOVE_DISTANCE == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_directions_match_the_lecture_headings() -> None:
|
||||||
|
assert constants.UP == 90
|
||||||
|
assert constants.DOWN == 270
|
||||||
|
assert constants.LEFT == 180
|
||||||
|
assert constants.RIGHT == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_four_directions_are_distinct() -> None:
|
||||||
|
directions = (constants.UP, constants.DOWN, constants.LEFT, constants.RIGHT)
|
||||||
|
|
||||||
|
assert len(set(directions)) == len(directions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_opposite_directions_differ_by_half_a_turn() -> None:
|
||||||
|
assert (constants.UP - constants.DOWN) % 360 == 180
|
||||||
|
assert (constants.LEFT - constants.RIGHT) % 360 == 180
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_delay_is_a_tenth_of_a_second() -> None:
|
||||||
|
assert constants.REFRESH_DELAY_SECONDS == 0.1
|
||||||
|
|
||||||
|
|
||||||
|
def test_food_is_a_small_blue_circle_without_animation() -> None:
|
||||||
|
assert constants.FOOD_SHAPE == "circle"
|
||||||
|
assert constants.FOOD_SIZE == 0.5
|
||||||
|
assert constants.FOOD_COLOR == "blue"
|
||||||
|
assert constants.FOOD_SPEED == "fastest"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_is_inside_the_screen() -> None:
|
||||||
|
assert 0 < constants.WALL_LIMIT < HALF_WIDTH
|
||||||
|
assert constants.WALL_LIMIT < HALF_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
def test_wall_is_280_pixels_from_the_centre() -> None:
|
||||||
|
assert constants.WALL_LIMIT == 280
|
||||||
|
|
||||||
|
|
||||||
|
def test_food_is_eaten_closer_than_15_pixels_which_is_less_than_a_move() -> None:
|
||||||
|
assert constants.FOOD_COLLISION_DISTANCE == 15
|
||||||
|
assert constants.FOOD_COLLISION_DISTANCE < constants.MOVE_DISTANCE
|
||||||
|
|
||||||
|
|
||||||
|
def test_scoreboard_is_white_text_at_the_top_centre_inside_the_screen() -> None:
|
||||||
|
x, y = constants.SCOREBOARD_POSITION
|
||||||
|
|
||||||
|
assert constants.SCOREBOARD_COLOR == "white"
|
||||||
|
assert constants.SCOREBOARD_ALIGNMENT == "center"
|
||||||
|
assert (x, y) == (0, 270)
|
||||||
|
assert abs(x) < HALF_WIDTH
|
||||||
|
assert 0 < y < HALF_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
def test_scoreboard_text_is_score_label_and_arial_24() -> None:
|
||||||
|
assert constants.SCORE_LABEL == "Score: "
|
||||||
|
assert constants.SCOREBOARD_FONT == ("Arial", 24, "normal")
|
||||||
|
|
||||||
|
|
||||||
|
def test_tail_is_touched_closer_than_10_pixels_which_is_less_than_a_move() -> None:
|
||||||
|
assert constants.TAIL_COLLISION_DISTANCE == 10
|
||||||
|
assert 0 < constants.TAIL_COLLISION_DISTANCE < constants.MOVE_DISTANCE
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_over_text_is_written_at_the_centre_inside_the_wall() -> None:
|
||||||
|
x, y = constants.GAME_OVER_POSITION
|
||||||
|
|
||||||
|
assert constants.GAME_OVER_TEXT == "GAME OVER"
|
||||||
|
assert (x, y) == (0, 0)
|
||||||
|
assert abs(x) < constants.WALL_LIMIT
|
||||||
|
assert abs(y) < constants.WALL_LIMIT
|
||||||
Reference in New Issue
Block a user