From abbba0c0aebf96b813248110e65c8f1af89bdaeb Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:13 +0800 Subject: [PATCH 1/8] Add pyproject.toml for the Day 24 repository Refs #2 Task: MIL-001#1 --- pyproject.toml | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..533fe89 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "snake-game" +version = "0.1.0" +description = "Snake game from Udemy's 100 Days of Code (day 24), built with Python turtle graphics: continuous play and a high score kept in a file between games." +readme = "README.md" +requires-python = ">=3.13" +license = "AGPL-3.0-only" +license-files = ["LICENSE"] +authors = [{ name = "Jens Tirsvad Nielsen" }] +keywords = ["snake", "game", "turtle", "udemy", "100-days-of-code", "high-score"] +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", + "Topic :: Education", + "Topic :: Games/Entertainment", +] +# The game runs on the standard library only (turtle needs tkinter). +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest>=8", + "ruff>=0.6", + "mypy>=1.11", +] + +[project.urls] +Repository = "https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/024-snake" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src", "tests"] + +[tool.ruff] +line-length = 88 +target-version = "py313" +src = ["src", "tests"] +extend-exclude = ["framework", "build"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP", "B", "SIM"] + +[tool.mypy] +python_version = "3.13" +strict = true +files = ["src", "tests"] +mypy_path = ["src", "tests"] -- 2.54.0 From c525a88fa4caa70977d1d7adc77ee07e2e25032b Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:13 +0800 Subject: [PATCH 2/8] Add the Python .gitignore for the Day 24 repository Refs #3 Task: MIL-001#2 --- .gitignore | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..165fe13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Personal tokens: never commit or import this file +.env + +# Virtual environment +.venv +venv + +# Byte-compiled and cached files +__pycache__ +*.py[cod] +*$py.class + +# Packaging and build output (build/ also holds the Doxygen HTML) +build/ +dist/ +*.egg-info/ +*.egg +.eggs/ +pip-wheel-metadata/ + +# Test, lint and type-check caches +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.coverage +.coverage.* +htmlcov/ + +# Editors and operating systems +.idea/ +.vscode/ +*.swp +.DS_Store +Thumbs.db +.codex/agents/ + +# Local state: the high score file of the game (day 24) +data.txt -- 2.54.0 From a78ed6e42e00592b0c1902620d639257bffba84f Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:14 +0800 Subject: [PATCH 3/8] Adopt constants.py and its test Refs #4 Task: MIL-001#3 --- src/snake_game/__init__.py | 14 ++++ src/snake_game/constants.py | 124 +++++++++++++++++++++++++++ tests/test_constants.py | 163 ++++++++++++++++++++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 src/snake_game/__init__.py create mode 100644 src/snake_game/constants.py create mode 100644 tests/test_constants.py diff --git a/src/snake_game/__init__.py b/src/snake_game/__init__.py new file mode 100644 index 0000000..5eb24c8 --- /dev/null +++ b/src/snake_game/__init__.py @@ -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).""" diff --git a/src/snake_game/constants.py b/src/snake_game/constants.py new file mode 100644 index 0000000..79ab1f2 --- /dev/null +++ b/src/snake_game/constants.py @@ -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) diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..b2d4315 --- /dev/null +++ b/tests/test_constants.py @@ -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 -- 2.54.0 From 0b6e0355633fdbe1fd1c488e46fa5f41f85a55f7 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:16 +0800 Subject: [PATCH 4/8] Add the README for the Day 24 repository Refs #5 Task: MIL-001#4 --- README.md | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e887db9..cd87313 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,157 @@ -# 024-snake +# Snake Game (Day 24) +The classic Snake game, built with Python's `turtle` module. It is the day-24 +assignment of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*. Day 24 +teaches the local file system and reading and writing files with `open` and the +`with` keyword, and applies them to the snake game: the scoreboard shows a high +score next to the score, the game goes on after a collision instead of ending, and +the high score is kept in the file `data.txt`, so it is still there the next time +the game is started. + +The game has no runtime dependencies. The code keeps the names of the lectures +(`Snake`, `create_snake`, `move`, `reset`, `up`, `down`, `left`, `right`, +`segments`, `head`, `game_is_on`, `Food`, `Scoreboard`, `refresh`, `extend`, +`increase_score`, `update_scoreboard`, `high_score`) so that you can compare it +with your own solution. + +This repository continues +[021-snake-game](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/021-snake-game) +and starts from its finished game; `docs/project-plan.md` tells how the work is +split into milestones. + +> **Status:** in progress. The repository foundation (this README, the +> configuration, the constants, the Doxygen settings and the continuous +> integration workflow) is in place; the game itself, continuous play, the high +> score and the high score file follow in milestones 2 to 4, and the wiki in +> milestone 5. The sections below describe the finished game. + +## Requirements + +- Python 3.13 or newer, with `tkinter` (the `turtle` module needs it). +- `git`, to clone the repository. +- Optional: [Doxygen](https://www.doxygen.nl/) to build the source documentation. +- No runtime dependencies. The development tools (`pytest`, `ruff`, `mypy`) are + installed into the virtual environment by the `dev` extra. + +## Set up + +Clone the repository, then create a local virtual environment `.venv` in its +root, upgrade `pip` and install the project with its development tools. The +`.venv` folder is ignored by git. + +```bash +git clone https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/024-snake.git +cd 024-snake +``` + +### Windows powershell + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +If PowerShell refuses to run the activation script, allow scripts for this +window only with `Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned` +and activate again. Python from python.org includes `tkinter`. + +### Linux debian + +Debian 13 or newer ships Python 3.13. On older releases install Python 3.13 +separately. + +```bash +sudo apt install python3 python3-venv python3-tk +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +### MacOS + +```bash +brew install python@3.13 python-tk@3.13 +python3.13 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +## Run + +With the virtual environment active: + +```bash +python -m snake_game +``` + +The game itself is added in milestone 2 (see the status above); this section +is completed with the controls, the two scores and the high score file in +milestones 2 to 5. + +## Run the tests + +The tests need no display: they use fakes instead of real turtles. + +```bash +pytest +``` + +The same checks that continuous integration runs: + +```bash +python -m pytest +python -m ruff check src tests +python -m ruff format --check src tests +python -m mypy +``` + +## Continuous integration + +`.gitea/workflows/ci.yml` runs on Gitea Actions on every push and on every pull +request. It sets up Python 3.13, upgrades `pip`, installs `.[dev]`, then runs +`pytest`, `ruff` (lint and format check) and `mypy`. It does not build the source +documentation: run `doxygen Doxyfile` yourself, as the next section shows. No +step opens a turtle window. + +The workflow lives in `.gitea/workflows` and not in `.github/workflows`, so +GitHub does not run it and pushing to a GitHub mirror needs no workflow +permission. + +## Build the source documentation + +The source is documented with Doxygen comments and the `Doxyfile` in the root. +Install Doxygen (`winget install DimitriVanHeesch.Doxygen` on Windows, +`sudo apt install doxygen` on Debian, `brew install doxygen` on macOS), then: + +```bash +doxygen Doxyfile +``` + +The HTML is written to `build/doxygen/index.html`. A warning fails the build. + +## Project layout + +```text +. +├── .gitea/workflows/ci.yml continuous integration (Gitea Actions) +├── docs/ business case, plan, milestones, reviews +├── src/snake_game/ the game +│ ├── __init__.py +│ └── constants.py every constant of the game +├── tests/ pytest tests +├── Doxyfile source documentation settings +├── LICENSE +├── pyproject.toml project configuration +└── README.md +``` + +The remaining modules (`snake.py`, `food.py`, `scoreboard.py`, `main.py` and +`__main__.py`) are added in milestone 2. + +## License + +GNU Affero General Public License v3.0 only. See [LICENSE](LICENSE). -- 2.54.0 From 079c40edee934965622a97fed5c19fbc1e4e96af Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:16 +0800 Subject: [PATCH 5/8] Add the Doxyfile for the Day 24 repository Refs #6 Task: MIL-001#5 --- Doxyfile | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Doxyfile diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..0bc0310 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,37 @@ +# Doxygen configuration for the snake game (Python sources in src/). +# +# Build: doxygen Doxyfile +# Output: build/doxygen/index.html +# +# Only the settings that differ from the Doxygen defaults are listed. A warning +# (for example an undocumented public item) fails the build. + +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = "Snake Game" +PROJECT_BRIEF = "Snake game from Udemy's 100 Days of Code (day 24)" +OUTPUT_DIRECTORY = build +HTML_OUTPUT = doxygen + +# Python sources are documented with Doxygen commands in docstrings and in +# "##" comments. +OPTIMIZE_OUTPUT_JAVA = YES +EXTRACT_ALL = NO +EXTRACT_PRIVATE = NO + +INPUT = src +FILE_PATTERNS = *.py +RECURSIVE = YES +SOURCE_BROWSER = YES + +GENERATE_HTML = YES +GENERATE_LATEX = NO +HAVE_DOT = NO + +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_IF_INCOMPLETE_DOC = YES +# NO: with YES, Doxygen also asks for an @return on every "-> None" function. +WARN_NO_PARAMDOC = NO +WARN_AS_ERROR = FAIL_ON_WARNINGS -- 2.54.0 From 1bec879a16e320fa8e4820a71ccce3cd6027cb4c Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:17 +0800 Subject: [PATCH 6/8] Add the continuous integration workflow Refs #7 Task: MIL-001#6 --- .gitea/workflows/ci.yml | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .gitea/workflows/ci.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..9b2ce32 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,46 @@ +# Continuous integration for the Snake Game. +# +# Run by Gitea Actions. The file lives in .gitea/workflows, not in +# .github/workflows: GitHub refuses a push that touches .github/workflows from a +# token without the workflow scope, and that blocks the push mirror to GitHub. +# Runs the same checks as the README, "Run the tests". +# +# The tests need no display: `snake_game.snake` does not import `turtle` until a +# real segment is created, and the tests pass fakes. +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + + - name: Tests + run: python -m pytest + + - name: Lint + run: python -m ruff check src tests + + - name: Format check + run: python -m ruff format --check src tests + + - name: Types + run: python -m mypy -- 2.54.0 From 61d6df778418312a964b8dba879177c201d28eaf Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:18 +0800 Subject: [PATCH 7/8] Record the review of the MIL-001 code (RC-009) --- docs/artifact-registry.md | 2 +- docs/sqa/reviews/rc-009-mil-001-code.md | 59 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 docs/sqa/reviews/rc-009-mil-001-code.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 82ed638..5e9452e 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g. | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 006 | | DICT | Domain Dictionary | docs/dictionary.md | 002 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 009 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 | ## Languages diff --git a/docs/sqa/reviews/rc-009-mil-001-code.md b/docs/sqa/reviews/rc-009-mil-001-code.md new file mode 100644 index 0000000..0891663 --- /dev/null +++ b/docs/sqa/reviews/rc-009-mil-001-code.md @@ -0,0 +1,59 @@ +# RC-009: Code review of MIL-001 (project foundation: constants, package, tests) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-009 | +| CrossReference | [MIL-001], [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 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: the source and tests that [MIL-001] delivers (project foundation: constants, package, tests) +- 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 | `SCREEN_WIDTH`, `BODY_COLOR` and the other constants are `UPPER_SNAKE`; test functions are `snake_case`; the package is `snake_game`. | +| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`STARTING_POSITIONS`, `FOOD_COLLISION_DISTANCE`); the only short names are `x` and `y` in tiny comprehensions. | +| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `python -m ruff check src tests` and `python -m ruff format --check src tests` exit 0 with the selection E, F, W, I, N, UP, B, SIM; no `noqa`. | +| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every test function is annotated `-> None`; `constants.py` holds `Final[...]` annotated values. | +| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | N-A | The changed files contain no exception handling. | +| 6 | No mutable default arguments and no shadowed builtins | Pass | No function has a default argument and no builtin is shadowed. | +| 7 | Files, locks and connections are managed with context managers | N-A | The changed files open no file. | +| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | `constants.py` has a Doxygen `##` comment for every constant; the module and the package have docstrings. | +| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | N-A | The changed files neither log nor print. | +| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | No class exists in these files; the classes of later milestones have no Design Class Diagram, a deviation recorded here: the project is a learning project of this size, and the classes trace to the lectures and to the base. | +| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | `tests/test_constants.py` has 27 tests named for the value they fix; none depends on order or the network. | +| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `python -m mypy` runs with `strict = true` and reports no issues; no `Any`. | +| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies = []`; the `dev` extra declares pytest, ruff and mypy with lower bounds, and all three are used. | +| 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 | Every value of the lectures is defined once, in `constants.py`; the tests read it from there. | +| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `constants.py` imports only `typing.Final`; 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 | A module of constants has one reason to change; there is no class to extend. | + +## 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-001]: ../../milestones/mil-001-project-foundation.md +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md -- 2.54.0 From 1cfbe6cf5c8e4db0f2e58bf0c69c013aaed1af0c Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:17:19 +0800 Subject: [PATCH 8/8] Resolve pending commit links --- docs/sqa/reviews/rc-009-mil-001-code.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sqa/reviews/rc-009-mil-001-code.md b/docs/sqa/reviews/rc-009-mil-001-code.md index 0891663..87a0953 100644 --- a/docs/sqa/reviews/rc-009-mil-001-code.md +++ b/docs/sqa/reviews/rc-009-mil-001-code.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-13 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-10-13 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [61d6df7] | --- @@ -57,3 +57,4 @@ Go — all Mandatory criteria pass or are not applicable. Assessed on 2026-10-13 [MIL-001]: ../../milestones/mil-001-project-foundation.md [QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md +[61d6df7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/024-snake/commit/61d6df778418312a964b8dba879177c201d28eaf -- 2.54.0