From c20ea6c6161717711a836e71d6c63d216a06f61d Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:38:50 +0800 Subject: [PATCH 1/6] Add the high score file constants Refs #23 Task: MIL-004#1 --- src/snake_game/constants.py | 12 ++++++++++++ tests/test_constants.py | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/snake_game/constants.py b/src/snake_game/constants.py index 270d76a..615409e 100644 --- a/src/snake_game/constants.py +++ b/src/snake_game/constants.py @@ -128,3 +128,15 @@ HIGH_SCORE_LABEL: Final[str] = "High Score: " # The place is far outside the 600 by 600 screen, so the old segments are out # of sight, as the day-24 lecture does it. SEGMENT_PARKING_POSITION: Final[tuple[int, int]] = (1000, 1000) + +## @brief Name of the text file that keeps the high score between games. +# +# A relative path: the file lies in the working directory the game is started +# from. The file is not tracked by git. +HIGH_SCORE_FILE: Final[str] = "data.txt" + +## @brief Text encoding used to read and write the high score file. +HIGH_SCORE_FILE_ENCODING: Final[str] = "utf-8" + +## @brief The high score of a new game, and of a file that cannot be read. +DEFAULT_HIGH_SCORE: Final[int] = 0 diff --git a/tests/test_constants.py b/tests/test_constants.py index d785adc..49c2b5f 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -164,6 +164,15 @@ def test_high_score_label_follows_the_score_label_as_in_the_lecture() -> None: assert constants.HIGH_SCORE_LABEL != constants.SCORE_LABEL +def test_high_score_is_kept_in_the_text_file_data_txt_as_utf_8() -> None: + assert constants.HIGH_SCORE_FILE == "data.txt" + assert constants.HIGH_SCORE_FILE_ENCODING == "utf-8" + + +def test_the_default_high_score_is_0() -> None: + assert constants.DEFAULT_HIGH_SCORE == 0 + + def test_old_segments_are_parked_outside_the_screen() -> None: x, y = constants.SEGMENT_PARKING_POSITION -- 2.54.0 From 4ef3874bb157bf5b5af79d7767c3ee8634cc20ca Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:38:51 +0800 Subject: [PATCH 2/6] Read and write the high score file and show the stored high score Refs #24 Refs #25 Refs #26 Task: MIL-004#2 Task: MIL-004#3 Task: MIL-004#4 --- src/snake_game/high_score_file.py | 53 +++++++++++++++++ src/snake_game/scoreboard.py | 20 +++++-- tests/fakes.py | 11 +++- tests/test_high_score_file.py | 94 +++++++++++++++++++++++++++++++ tests/test_scoreboard.py | 80 ++++++++++++++++++++++++++ 5 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 src/snake_game/high_score_file.py create mode 100644 tests/test_high_score_file.py diff --git a/src/snake_game/high_score_file.py b/src/snake_game/high_score_file.py new file mode 100644 index 0000000..5037aee --- /dev/null +++ b/src/snake_game/high_score_file.py @@ -0,0 +1,53 @@ +"""! @file +@brief Reading and writing the high score file: the day-24 file lectures. + +The high score is kept as text in a file, so that it is still there the next time +the game is started. Both functions open the file with a `with` statement, which +closes it again even when something goes wrong, as the lecture teaches. The module +does not import `turtle`, so it needs no display and the tests use a temporary +file. +""" + +from snake_game.constants import ( + DEFAULT_HIGH_SCORE, + HIGH_SCORE_FILE, + HIGH_SCORE_FILE_ENCODING, +) + + +def write_high_score(high_score: int, path: str = HIGH_SCORE_FILE) -> None: + """! @brief Replace the content of the file with the high score, as text. + + The file is opened in write mode `w`, so the old content is overwritten and a + file that does not exist yet is created. + + @param high_score The high score to store. + @param path The file to write; defaults to `HIGH_SCORE_FILE`. + """ + with open(path, mode="w", encoding=HIGH_SCORE_FILE_ENCODING) as data: + data.write(str(high_score)) + + +def read_high_score(path: str = HIGH_SCORE_FILE) -> int: + """! @brief Read the high score from the file and turn the text into a number. + + A file that does not exist yet is created holding `DEFAULT_HIGH_SCORE`. A file + that does not hold a whole number, or holds a negative one, is read as + `DEFAULT_HIGH_SCORE` and is left as it is; the next new high score replaces it. + + @param path The file to read; defaults to `HIGH_SCORE_FILE`. + @return The stored high score, 0 or more. + """ + try: + with open(path, encoding=HIGH_SCORE_FILE_ENCODING) as data: + text = data.read() + except FileNotFoundError: + write_high_score(DEFAULT_HIGH_SCORE, path) + return DEFAULT_HIGH_SCORE + except UnicodeDecodeError: + return DEFAULT_HIGH_SCORE + try: + high_score = int(text) + except ValueError: + return DEFAULT_HIGH_SCORE + return max(high_score, DEFAULT_HIGH_SCORE) diff --git a/src/snake_game/scoreboard.py b/src/snake_game/scoreboard.py index 7513098..11925f8 100644 --- a/src/snake_game/scoreboard.py +++ b/src/snake_game/scoreboard.py @@ -10,6 +10,7 @@ module first. from turtle import Turtle from snake_game.constants import ( + HIGH_SCORE_FILE, HIGH_SCORE_LABEL, SCORE_LABEL, SCOREBOARD_ALIGNMENT, @@ -17,18 +18,25 @@ from snake_game.constants import ( SCOREBOARD_FONT, SCOREBOARD_POSITION, ) +from snake_game.high_score_file import read_high_score, write_high_score class Scoreboard(Turtle): """! @brief Shows the score as text at the top centre of the screen.""" - def __init__(self) -> None: - """! @brief Set the scoreboard up and write the score 0.""" + def __init__(self, path: str = HIGH_SCORE_FILE) -> None: + """! @brief Set the scoreboard up and write the score 0 and the stored best. + + @param path The file that keeps the high score; defaults to + `HIGH_SCORE_FILE`. + """ super().__init__() + ## @brief The file that keeps the high score between games. + self.path = path ## @brief The number of foods the snake has eaten since the last reset. self.score = 0 - ## @brief The largest score reached so far. - self.high_score = 0 + ## @brief The largest score reached so far, as stored in the file. + self.high_score = read_high_score(path) self.color(SCOREBOARD_COLOR) self.penup() self.hideturtle() @@ -53,9 +61,11 @@ class Scoreboard(Turtle): """! @brief Keep the best score, start a new game and write the scoreboard. When the score is higher than the high score, it becomes the new high - score. The score then goes back to 0. + score and is written to the file; a lower or equal score does not write + the file. The score then goes back to 0. """ if self.score > self.high_score: self.high_score = self.score + write_high_score(self.high_score, self.path) self.score = 0 self.update_scoreboard() diff --git a/tests/fakes.py b/tests/fakes.py index d291f1c..ab97356 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -286,16 +286,23 @@ def make_food(monkeypatch: pytest.MonkeyPatch) -> Any: return module.Food() -def make_scoreboard(monkeypatch: pytest.MonkeyPatch) -> Any: +def make_scoreboard( + monkeypatch: pytest.MonkeyPatch, path: str | Path | None = None +) -> Any: """Make a real `Scoreboard` whose base class is the fake `Turtle`. + When `path` is given it is the high score file; otherwise the default + `data.txt` is used, which `conftest.py` has moved into a temporary directory. + The result is typed `Any` because it has the methods of `Scoreboard` (`increase_score`) and the recording methods of the fake base class (`calls`) at once. """ install_fake_turtle(monkeypatch) module = importlib.import_module("snake_game.scoreboard") - return module.Scoreboard() + if path is None: + return module.Scoreboard() + return module.Scoreboard(str(path)) def imports_turtle_or_tkinter(module_name: str) -> bool: diff --git a/tests/test_high_score_file.py b/tests/test_high_score_file.py new file mode 100644 index 0000000..449f0b8 --- /dev/null +++ b/tests/test_high_score_file.py @@ -0,0 +1,94 @@ +"""Tests of reading and writing the high score file, with temporary files.""" + +from pathlib import Path + +from snake_game.high_score_file import read_high_score, write_high_score + + +def test_read_returns_the_stored_number(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_text("17", encoding="utf-8") + + assert read_high_score(str(path)) == 17 + + +def test_read_accepts_a_trailing_newline(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_text("5\n", encoding="utf-8") + + assert read_high_score(str(path)) == 5 + + +def test_read_creates_a_missing_file_holding_0(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + + assert read_high_score(str(path)) == 0 + assert path.read_text(encoding="utf-8") == "0" + + +def test_read_gives_0_for_an_empty_file_and_leaves_it_empty(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_text("", encoding="utf-8") + + assert read_high_score(str(path)) == 0 + assert path.read_text(encoding="utf-8") == "" + + +def test_read_gives_0_for_text_that_is_not_a_whole_number(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_text("many", encoding="utf-8") + + assert read_high_score(str(path)) == 0 + assert path.read_text(encoding="utf-8") == "many" + + +def test_read_gives_0_for_a_negative_number(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_text("-4", encoding="utf-8") + + assert read_high_score(str(path)) == 0 + assert path.read_text(encoding="utf-8") == "-4" + + +def test_read_gives_0_for_bytes_that_are_not_utf_8(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_bytes(b"\xff\xfe\x00\x80") + + assert read_high_score(str(path)) == 0 + assert path.read_bytes() == b"\xff\xfe\x00\x80" + + +def test_write_replaces_the_old_content(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + path.write_text("123456", encoding="utf-8") + + write_high_score(9, str(path)) + + assert path.read_text(encoding="utf-8") == "9" + + +def test_write_creates_the_file(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + + write_high_score(3, str(path)) + + assert path.read_text(encoding="utf-8") == "3" + + +def test_the_file_is_closed_after_reading_and_writing(tmp_path: Path) -> None: + path = tmp_path / "data.txt" + + write_high_score(2, str(path)) + read_high_score(str(path)) + path.unlink() # fails on Windows while a handle is still open + + assert not path.exists() + + +def test_the_default_path_is_data_txt_in_the_working_directory( + tmp_path: Path, +) -> None: + write_high_score(4) + + assert (tmp_path / "data.txt").read_text(encoding="utf-8") == "4" + assert read_high_score() == 4 diff --git a/tests/test_scoreboard.py b/tests/test_scoreboard.py index a7e1c2b..22c64f6 100644 --- a/tests/test_scoreboard.py +++ b/tests/test_scoreboard.py @@ -2,6 +2,7 @@ import importlib import sys +from pathlib import Path import pytest @@ -129,6 +130,85 @@ def test_the_score_text_shows_both_scores_while_playing( assert scoreboard.calls[-1][1][0] == "Score: 1 High Score: 2" +def test_scoreboard_shows_the_stored_high_score_at_start( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + path = tmp_path / "scores.txt" + path.write_text("12", encoding="utf-8") + + scoreboard = make_scoreboard(monkeypatch, path) + + assert scoreboard.high_score == 12 + assert scoreboard.calls[-1][1][0] == "Score: 0 High Score: 12" + + +def test_a_new_high_score_is_written_to_the_file_on_reset( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + path = tmp_path / "scores.txt" + path.write_text("1", encoding="utf-8") + scoreboard = make_scoreboard(monkeypatch, path) + for _ in range(2): + scoreboard.increase_score() + + scoreboard.reset() + + assert path.read_text(encoding="utf-8") == "2" + + +def test_a_lower_score_does_not_write_the_file( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + path = tmp_path / "scores.txt" + path.write_text("5", encoding="utf-8") + scoreboard = make_scoreboard(monkeypatch, path) + scoreboard.increase_score() + path.write_text("sentinel", encoding="utf-8") + + scoreboard.reset() + + assert path.read_text(encoding="utf-8") == "sentinel" + assert scoreboard.high_score == 5 + + +def test_an_equal_score_does_not_write_the_file( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + path = tmp_path / "scores.txt" + path.write_text("2", encoding="utf-8") + scoreboard = make_scoreboard(monkeypatch, path) + scoreboard.increase_score() + scoreboard.increase_score() + path.write_text("sentinel", encoding="utf-8") + + scoreboard.reset() + + assert path.read_text(encoding="utf-8") == "sentinel" + + +def test_a_missing_file_shows_high_score_0_and_is_created( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + path = tmp_path / "scores.txt" + + scoreboard = make_scoreboard(monkeypatch, path) + + assert scoreboard.calls[-1] == START_TEXT + assert path.read_text(encoding="utf-8") == "0" + + +def test_a_damaged_file_shows_high_score_0( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + path = tmp_path / "scores.txt" + path.write_text("not a number", encoding="utf-8") + + scoreboard = make_scoreboard(monkeypatch, path) + + assert scoreboard.high_score == 0 + assert scoreboard.calls[-1] == START_TEXT + + def test_scoreboard_has_no_game_over_any_more( monkeypatch: pytest.MonkeyPatch, ) -> None: -- 2.54.0 From ba0ae0e6982acf12c483d48d8d6f41105b6ddb79 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:38:54 +0800 Subject: [PATCH 3/6] Keep the tests away from the real data.txt Refs #27 Task: MIL-004#5 --- tests/conftest.py | 16 ++++++++++++++++ tests/test_main.py | 12 ++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..620e26c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +"""Shared pytest configuration for the snake game tests.""" + +from pathlib import Path + +import pytest + + +@pytest.fixture(autouse=True) +def isolated_working_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Run every test in an empty temporary working directory. + + The high score file has a relative default path (`data.txt`). Without this + fixture, a test that uses the default would read and write the real file of + the repository or of the directory pytest was started from. + """ + monkeypatch.chdir(tmp_path) diff --git a/tests/test_main.py b/tests/test_main.py index 60aaed6..0c11448 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,7 @@ """Tests of the main flow, with fakes instead of a real screen.""" +from pathlib import Path + import pytest from fakes import ( @@ -131,6 +133,16 @@ def test_main_sets_up_the_screen_turns_off_drawing_and_binds_the_keys( assert sorted(screens[0].bindings) == sorted(ARROW_KEYS) +def test_main_creates_data_txt_in_the_working_directory_holding_0( + monkeypatch: pytest.MonkeyPatch, sleeps: list[float], tmp_path: Path +) -> None: + install_fake_turtle(monkeypatch) + + main() + + assert (tmp_path / constants.HIGH_SCORE_FILE).read_text(encoding="utf-8") == "0" + + def test_register_head_shape_builds_two_components_from_the_constants( monkeypatch: pytest.MonkeyPatch, ) -> None: -- 2.54.0 From b353db8b897801abbb16521f94e5cc108b9d781d Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:38:55 +0800 Subject: [PATCH 4/6] Document the high score file in the README Refs #28 Task: MIL-004#6 --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1679a09..aa7b7d2 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,10 @@ This repository continues and starts from its finished game; `docs/project-plan.md` tells how the work is split into milestones. -> **Status:** in progress. The game of day 21 is adopted and now follows the -> day-24 lectures on continuous play: the scoreboard shows the score and the high -> score, and a collision resets the snake and the score instead of ending the -> game. The high score is kept in memory only; the high score file (milestone 4) -> and the wiki (milestone 5) follow. The sections below describe the game as it -> is now; they are updated in each milestone. +> **Status:** in progress. The game of day 21 is adopted, follows the day-24 +> lectures on continuous play and the high score, and now keeps the high score in +> the file `data.txt`. The wiki in three languages (milestone 5) follows. The +> sections below describe the game as it is now. ## Requirements @@ -113,8 +111,18 @@ The game does not end. When the head passes the wall (more than 280 pixels from the centre on any side) or touches the tail, the old snake is sent off the screen, a new snake of three segments appears in the middle, the score goes back to 0, and the high score is raised if the score was higher. Close the window with -its close button when you have had enough. The high score is lost when the game -is closed until milestone 4 stores it in a file. +its close button when you have had enough. + +### The high score file + +The high score is kept as text in the file `data.txt`, in the folder you start +the game from (`python -m snake_game`). The game reads it when it starts and +writes it again each time a new high score is reached, so the high score is +still there the next time you play. The file is created holding `0` the first +time, and a file that is empty or does not hold a whole number is read as `0`. +To start from zero again, close the game and delete `data.txt`. The file is +your own state and is ignored by git. Start the game from another folder and it +uses the `data.txt` of that folder. ## Run the tests @@ -168,10 +176,12 @@ The HTML is written to `build/doxygen/index.html`. A warning fails the build. │ ├── __main__.py starts the game: python -m snake_game │ ├── constants.py every constant of the game │ ├── food.py the Food class (inherits from Turtle) +│ ├── high_score_file.py read_high_score and write_high_score (data.txt) │ ├── main.py screen set-up and the main flow │ ├── scoreboard.py the Scoreboard class (inherits from Turtle) │ └── snake.py the Snake class -├── tests/ pytest tests (fakes.py holds the fake turtle and screen) +├── tests/ pytest tests (fakes.py holds the fake turtle and screen; +│ conftest.py runs every test in a temporary folder) ├── Doxyfile source documentation settings ├── LICENSE ├── pyproject.toml project configuration -- 2.54.0 From 5dcd51a47a16f28181a649bff9a9104d524ab278 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:38:56 +0800 Subject: [PATCH 5/6] Record the review of the MIL-004 code (RC-012) --- docs/artifact-registry.md | 2 +- docs/sqa/reviews/rc-012-mil-004-code.md | 59 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 docs/sqa/reviews/rc-012-mil-004-code.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index eeb680f..e1c53ba 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 | 012 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 013 | ## Languages diff --git a/docs/sqa/reviews/rc-012-mil-004-code.md b/docs/sqa/reviews/rc-012-mil-004-code.md new file mode 100644 index 0000000..2096ab8 --- /dev/null +++ b/docs/sqa/reviews/rc-012-mil-004-code.md @@ -0,0 +1,59 @@ +# RC-012: Code review of MIL-004 (high score file) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-012 | +| CrossReference | [MIL-004], [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-004] delivers (high score file) +- 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 | `read_high_score`, `write_high_score`, `high_score_file` and the constants `HIGH_SCORE_FILE`, `DEFAULT_HIGH_SCORE` follow PEP 8. | +| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose; `path`, `data` and `text` are the lecture's own and are used in short scopes. | +| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` exit 0; no `noqa`. | +| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | `mypy` strict reports no issues in 16 files; every new def is annotated. | +| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | Only `FileNotFoundError`, `UnicodeDecodeError` and `ValueError` are caught, each with a defined result (create the file with 0 and return 0; read as 0); other `OSError`s propagate; none is swallowed without a defined result. | +| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable default argument; no shadowed builtin (`open` is called, not rebound). | +| 7 | Files, locks and connections are managed with context managers | Pass | Both functions use `with open(...) as data`, as the lecture teaches; a test checks that the file can be replaced afterwards. | +| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Both functions, the module and the new constants have Doxygen docstrings; `doxygen Doxyfile` ends with 0 warnings. | +| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` and no logging. | +| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | No Design Class Diagram exists (deviation recorded in RC-010); `high_score_file.py` is a function module and `Scoreboard` gains a `path` and the two calls, as the lecture does with `data.txt`. | +| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 169 tests; `test_high_score_file.py` covers stored, newline, missing, empty, non-numeric, negative, undecodable, replace and create; `conftest.py` runs every test in a temporary folder so no test touches a real `data.txt`; no test depends on order or the network. | +| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `strict = true`; no `Any` was added. | +| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies = []` is unchanged. | +| 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 | The file name, encoding and the fallback value are defined once in `constants.py`; reading and writing the file live only in `high_score_file.py`. | +| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `high_score_file.py` imports only `constants`; `scoreboard.py` imports it; no cycle; the module needs no `turtle`. | +| 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 | Reading and writing are separate functions with one reason each; `Scoreboard` receives the path as a parameter, so tests pass a temporary file. | + +## 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-004]: ../../milestones/mil-004-high-score-file.md +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md -- 2.54.0 From 4ae80b1f954fc2e047d939ec3d1a26bf59719c62 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 14:38:57 +0800 Subject: [PATCH 6/6] Resolve pending commit links --- docs/sqa/reviews/rc-012-mil-004-code.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sqa/reviews/rc-012-mil-004-code.md b/docs/sqa/reviews/rc-012-mil-004-code.md index 2096ab8..3bbb78e 100644 --- a/docs/sqa/reviews/rc-012-mil-004-code.md +++ b/docs/sqa/reviews/rc-012-mil-004-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 | [5dcd51a] | --- @@ -57,3 +57,4 @@ Go — all Mandatory criteria pass or are not applicable. Assessed on 2026-10-13 [MIL-004]: ../../milestones/mil-004-high-score-file.md [QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md +[5dcd51a]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/024-snake/commit/5dcd51a47a16f28181a649bff9a9104d524ab278 -- 2.54.0