MIL-001: Project foundation #35
@@ -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
|
||||||
+38
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -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 | 009 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 |
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 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 | [61d6df7] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
[61d6df7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/024-snake/commit/61d6df778418312a964b8dba879177c201d28eaf
|
||||||
@@ -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"]
|
||||||
@@ -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