Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9c1a0a09 | ||
|
|
1334730f8e | ||
|
|
daee0f5c98 | ||
|
|
79d008fb70 | ||
|
|
45c0cf8acf | ||
|
|
54f60a951a | ||
|
|
4cb75dff77 | ||
|
|
88aee4051d | ||
|
|
665301689d | ||
|
|
76ac50e591 | ||
|
|
575df4d07f | ||
|
|
c893fc1a17 | ||
|
|
eb73bb33e4 | ||
|
|
4d7cc33cac | ||
|
|
795b9dba39 | ||
|
|
16183ccff6 | ||
|
|
3e8269c4fb | ||
|
|
b5483a7373 | ||
|
|
2328490945 | ||
|
|
949d2edb63 | ||
|
|
c695c47929 | ||
|
|
32861fc58f | ||
|
|
9318a07fa0 |
@@ -0,0 +1,43 @@
|
||||
# Continuous integration for the Quiz Game.
|
||||
#
|
||||
# Run by Gitea Actions. The file lives in .gitea/workflows, not .github/workflows:
|
||||
# GitHub refuses pushes that touch .github/workflows from a token without the
|
||||
# workflow scope, and that stopped the push mirror to GitHub. Runs the same
|
||||
# checks as the README, "Run the tests".
|
||||
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 the development tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --group dev
|
||||
|
||||
- name: Lint
|
||||
run: ruff check src tests
|
||||
|
||||
- name: Check formatting
|
||||
run: ruff format --check src tests
|
||||
|
||||
- name: Check types
|
||||
run: mypy --strict src tests
|
||||
|
||||
- name: Run the tests
|
||||
run: python -m pytest
|
||||
@@ -1,44 +0,0 @@
|
||||
name: CI
|
||||
|
||||
# Read by GitHub Actions and by Gitea Actions. No step opens a turtle window:
|
||||
# the tests use a recording fake pen.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
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 Doxygen
|
||||
run: sudo apt-get update && sudo apt-get install --yes doxygen
|
||||
|
||||
- name: Install the project and its development tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
|
||||
- name: Lint
|
||||
run: ruff check .
|
||||
|
||||
- name: Check formatting
|
||||
run: ruff format --check .
|
||||
|
||||
- name: Check types
|
||||
run: mypy
|
||||
|
||||
- name: Run the tests
|
||||
run: python -m pytest
|
||||
|
||||
- name: Build the source documentation
|
||||
run: doxygen Doxyfile
|
||||
@@ -75,8 +75,15 @@ python -m turtle_challenges dashed-line
|
||||
| --- | --- | --- |
|
||||
| 1. Draw a square | `turtle-challenges square` | `draw_square` |
|
||||
| 2. Draw a dashed line | `turtle-challenges dashed-line` | `draw_dashed_line` |
|
||||
| 3. Draw different shapes | `turtle-challenges shapes` | `draw_shape`, `draw_shapes`, `random_color` |
|
||||
| 4. Generate a random walk | `turtle-challenges random-walk` | `random_walk` |
|
||||
| 5. Draw a spirograph | `turtle-challenges spirograph` | `draw_spirograph` |
|
||||
|
||||
Challenges 3 to 5 are added by milestones 003 and 004.
|
||||
The spirograph takes the number of degrees between two circles with `--gap` (default 5):
|
||||
|
||||
```bash
|
||||
turtle-challenges spirograph --gap 10
|
||||
```
|
||||
|
||||
`turtle-challenges --help` lists the challenges. If the command prints that the turtle module needs Tk, install Tk as described under Requirements.
|
||||
|
||||
@@ -132,8 +139,12 @@ Open `build/html/index.html` in a browser. Any Doxygen warning fails the build,
|
||||
│ ├── constants.py every constant of the project
|
||||
│ ├── pen.py the Pen and Window protocols the challenges use
|
||||
│ ├── window.py opens the turtle window and creates the turtle
|
||||
│ ├── colors.py random_color and random_palette_color
|
||||
│ ├── square.py challenge 1: draw_square
|
||||
│ ├── dashed_line.py challenge 2: draw_dashed_line
|
||||
│ ├── shapes.py challenge 3: draw_shape, draw_shapes
|
||||
│ ├── random_walk.py challenge 4: random_walk
|
||||
│ ├── spirograph.py challenge 5: draw_spirograph
|
||||
│ └── cli.py the turtle-challenges command
|
||||
├── tests/ pytest tests and the recording fake pen
|
||||
├── Doxyfile Doxygen configuration
|
||||
|
||||
@@ -15,7 +15,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 014 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 019 |
|
||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
||||
|
||||
|
||||
+13
-7
@@ -11,7 +11,8 @@
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [4b3391b] |
|
||||
| 2026-10-08 | Deprecated | Jens Tirsvad Nielsen | S01 | Accepted by S01, the Product Owner<br>Reviews open issue updated | [16183cc] |
|
||||
| 2026-10-08 | Accepted | Jens Tirsvad Nielsen | S01 | Reviews open issue updated after the code reviews (`RC-014` to `RC-017`)<br>Accepted by S01, the Product Owner | [c893fc1] |
|
||||
|
||||
---
|
||||
|
||||
@@ -31,10 +32,10 @@ This plan schedules the four milestones that deliver the Turtle Challenges repos
|
||||
|
||||
| Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Project Foundation | [MIL-001] | 2026-10-07 to 2026-10-10 | 2026-10-10 | S01 | none | `pyproject.toml`, `.gitignore`, package and test skeleton, `Doxyfile`, continuous integration, README, repository description and topics | |
|
||||
| Square and Dashed Line | [MIL-002] | 2026-10-11 to 2026-10-14 | 2026-10-14 | S01 | none | `Pen` protocol, `draw_square`, `draw_dashed_line`, fake turtle, tests, command line entry | |
|
||||
| Shapes and Random Color | [MIL-003] | 2026-10-15 to 2026-10-17 | 2026-10-17 | S01 | none | `random_color`, color palette, `draw_shape`, `draw_shapes`, tests | |
|
||||
| Random Walk and Spirograph | [MIL-004] | 2026-10-18 to 2026-10-21 | 2026-10-21 | S01 | none | `random_walk`, `draw_spirograph`, tests, final README | |
|
||||
| Project Foundation | [MIL-001] | 2026-10-07 to 2026-10-10 | 2026-10-10 | S01 | none | `pyproject.toml`, `.gitignore`, package and test skeleton, `Doxyfile`, continuous integration, README, repository description and topics | [MIL-001 milestone] |
|
||||
| Square and Dashed Line | [MIL-002] | 2026-10-11 to 2026-10-14 | 2026-10-14 | S01 | none | `Pen` protocol, `draw_square`, `draw_dashed_line`, fake turtle, tests, command line entry | [MIL-002 milestone] |
|
||||
| Shapes and Random Color | [MIL-003] | 2026-10-15 to 2026-10-17 | 2026-10-17 | S01 | none | `random_color`, color palette, `draw_shape`, `draw_shapes`, tests | [MIL-003 milestone] |
|
||||
| Random Walk and Spirograph | [MIL-004] | 2026-10-18 to 2026-10-21 | 2026-10-21 | S01 | none | `random_walk`, `draw_spirograph`, tests, final README | [MIL-004 milestone] |
|
||||
|
||||
```plantuml
|
||||
@startgantt
|
||||
@@ -93,7 +94,7 @@ Each milestone needs the previous one `Accepted` with a Go review. A No-Go retur
|
||||
- **Development tools:** ruff and mypy are added as development dependencies, beyond the pytest the brief names, because `QC-PY-001` requires a formatter, a linter and (optionally) a strict type check. They are not runtime dependencies.
|
||||
- **Continuous integration host:** the workflow lives in `.github/workflows/ci.yml`, which GitHub Actions and Gitea Actions both read. Whether the Gitea host has a runner is not known.
|
||||
- **Diagram:** the PlantUML Gantt chart is not rendered yet because no PlantUML server is configured (`render-diagrams.sh --server <url>`).
|
||||
- **Reviews:** [BC-001], [SA-001], `DICT-001` and the four milestones were reviewed and ended in `Go` on 2026-10-07 (`RC-007` to `RC-013`, which re-review `RC-001` to `RC-006`); their Version History rows are `Accepted`. S01 accepted, in chat, that the author is also the reviewer, because the project has one person and no governance document exists. This plan has no checklist, so S01 accepts it directly: its row is still `Proposed`. S01 waived the plan-first gate in chat on 2026-10-07 for the first build, so the code of all four milestones was written before any document was reviewed; the waiver does not carry over to the next request. The code has not been reviewed against `QC-PY-001` yet.
|
||||
- **Reviews:** [BC-001], [SA-001], `DICT-001` and the four milestones were reviewed and ended in `Go` on 2026-10-07 (`RC-007` to `RC-013`, which re-review `RC-001` to `RC-006`); their Version History rows are `Accepted`. The code of the four milestones was reviewed against `QC-PY-001` and ended in `Go` on 2026-10-08 (`RC-014` to `RC-017`). S01 accepted, in chat, that the author is also the reviewer, because the project has one person and no governance document exists. This plan has no checklist; S01 accepted it directly on 2026-10-08. S01 waived the plan-first gate in chat on 2026-10-07 for the first build, so the code of all four milestones was written before any document was reviewed; the waiver does not carry over to the next request.
|
||||
- **GitHub:** issues are synced to the `origin` remote (the Gitea host) only; the GitHub mirror gets the description and topics but no milestones or issues.
|
||||
|
||||
---
|
||||
@@ -104,4 +105,9 @@ Each milestone needs the previous one `Accepted` with a Go review. A No-Go retur
|
||||
[MIL-002]: ./milestones/mil-002-square-and-dashed-line.md
|
||||
[MIL-003]: ./milestones/mil-003-shapes-and-random-color.md
|
||||
[MIL-004]: ./milestones/mil-004-random-walk-and-spirograph.md
|
||||
[4b3391b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/4b3391b22626b49aca06a7c265c1d0be93d1155d
|
||||
[MIL-001 milestone]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/milestone/70
|
||||
[MIL-002 milestone]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/milestone/71
|
||||
[MIL-003 milestone]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/milestone/72
|
||||
[MIL-004 milestone]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/milestone/73
|
||||
[16183cc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/16183ccff68a82c6a97c00333447c6faefa11503
|
||||
[c893fc1]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/c893fc1a1780a0d0e7a023fde00f42e6b4eaffb7
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Review Record: Code of Milestone 001
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-014 |
|
||||
| CrossReference | [MIL-001], [QC-PY-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [4d7cc33] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: Python source code of milestone 001 (type `PY`): the Python files that this milestone added or changed, on branch `mil-001-project-foundation` at commit d38f114
|
||||
- Checklist used: [QC-PY-001] (`QC-PY-001`, Python Source Code)
|
||||
- Milestone: [MIL-001], tasks 1 to 3 (#1 to #3), plus the configuration of tasks 1 and 2 (`pyproject.toml`, `.gitignore`); this record is the review task, issue #8
|
||||
- Scope: full review of the 4 Python files this milestone added or changed: `src/turtle_challenges/__init__.py`, `src/turtle_challenges/constants.py`, `tests/__init__.py`, `tests/test_package.py`. `pyproject.toml` and `.gitignore` are reviewed as the milestone's configuration.
|
||||
- Toolchain at that commit, run in a clean checkout of the branch: `ruff format --check .` and `ruff check .` exit 0, `mypy` reports no issues in 4 source files, `pytest` reports 3 passed, and `doxygen Doxyfile` prints no warning.
|
||||
- Language and domain: n/a (technical type)
|
||||
- Language reviewer: none (n/a)
|
||||
- Reviewer eligibility: S01 is the author of this code (the assistant wrote it for S01) and also its reviewer, which `framework/process/review-checklist-process.md` does not allow. S01 accepted this deviation in chat on 2026-10-07; it is recorded in the traceability matrix.
|
||||
- Status of this record: decided on 2026-10-08 by S01, the reviewer. The statuses and evidence are the assistant's assessment from the toolchain, an AST scan and a reading of the code, confirmed by S01.
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | The package and modules (`turtle_challenges`, `constants`, `test_package`) are `snake_case`, the constants `WINDOW_TITLE` and `COLOR_MODE` are `UPPER_SNAKE`, the test functions are `snake_case`. The ruff naming rules (`N`) are on and report nothing. |
|
||||
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | `WINDOW_TITLE` and `COLOR_MODE` say what they hold, and each constant has a comment that says what it is for. The scan finds no single-letter and no short names. |
|
||||
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format --check .` and `ruff check .` exit 0 with the rules `ANN`, `B`, `E`, `F`, `I`, `N`, `SIM`, `UP` and `W`; `pyproject.toml` commits the configuration. No `# noqa` and no `# type: ignore` (scan). |
|
||||
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | The only functions are the three smoke tests, annotated `-> None`; ruff `ANN` and mypy strict report nothing. |
|
||||
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | N-A | No code in this milestone raises or catches an exception. |
|
||||
| 6 | No mutable default arguments and no shadowed builtins | Pass | Scan of the changed files: no mutable default argument and no shadowed builtin (parameters, variables, functions, classes). |
|
||||
| 7 | Files, locks and connections are managed with context managers | N-A | No file, lock or connection is opened (scan: no `open(`). |
|
||||
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Optional. `__init__.py` and `constants.py` have module docstrings and every constant has a Doxygen comment. `doxygen Doxyfile` has 0 warnings, and a warning fails the build. |
|
||||
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` call (scan) and no logging; nothing reads `.env` or any other secret. |
|
||||
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No class or operation is defined, and no design class diagram exists: the milestone's tasks are plain technical tasks (`Needs its own Use Case/User Story?` is No). S01 confirmed `N-A` on 2026-10-08. |
|
||||
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | `tests/test_package.py` has 3 smoke tests named for the behaviour (the package imports and is documented, the color mode, the window title). They pass alone and in reverse order and use no network (scan). |
|
||||
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | Optional. `mypy` (strict, set in `pyproject.toml`) reports no issues in 4 source files; `Any` is not used. |
|
||||
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Fail | Optional. `pyproject.toml` declares `mypy>=1.13`, `pytest>=8.3`, `ruff>=0.8` and the build backend `setuptools>=77` with lower bounds only; nothing is pinned and there is no lockfile or constraints file. The runtime dependency list is empty, so no dependency is unused. S01 accepted lower bounds on 2026-10-08, so this optional failure stays recorded and needs no action. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go — all mandatory criteria of `QC-PY-001` pass or are `N-A`. The optional criterion 13 fails (lower bounds only); S01 accepted that on 2026-10-08, so it needs no action.
|
||||
|
||||
Reviewer eligibility: S01 is the author of this code and also its reviewer, which the review process does not allow; S01 accepted this deviation in chat on 2026-10-07 and it is recorded in the traceability matrix.
|
||||
|
||||
The row of the code in the traceability matrix is updated, and issue #8 closes through the commit message when the pull request that carries it merges into `main`.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Optional (criterion 13): pin the development tools (exact versions or a constraints file), or record that lower bounds are accepted. Closed 2026-10-08: S01 accepted lower bounds. | S01 | 2026-10-10 |
|
||||
| After the verdict `Go`: update the traceability matrix and close issue #8. Done 2026-10-08: the matrix is updated and the commit message carries `Closes #8`. | S01 | 2026-10-10 |
|
||||
|
||||
---
|
||||
|
||||
[MIL-001]: ../../milestones/mil-001-project-foundation.md
|
||||
[QC-PY-001]: ../../../framework/qc/qc-programming-python.md
|
||||
[4d7cc33]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/4d7cc33cac2c090c663409abb1e7ab0a34d871f5
|
||||
@@ -0,0 +1,68 @@
|
||||
# Review Record: Code of Milestone 002
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-015 |
|
||||
| CrossReference | [MIL-002], [QC-PY-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [4d7cc33] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: Python source code of milestone 002 (type `PY`): the Python files that this milestone added or changed, on branch `mil-002-square-and-dashed-line` at commit b3153b9
|
||||
- Checklist used: [QC-PY-001] (`QC-PY-001`, Python Source Code)
|
||||
- Milestone: [MIL-002], tasks 1 to 6 (#9 to #14); this record is the review task, issue #15
|
||||
- Scope: full review of the 14 Python files this milestone added or changed: `src/turtle_challenges/__init__.py`, `src/turtle_challenges/__main__.py`, `src/turtle_challenges/cli.py`, `src/turtle_challenges/constants.py`, `src/turtle_challenges/dashed_line.py`, `src/turtle_challenges/pen.py`, `src/turtle_challenges/square.py`, `src/turtle_challenges/window.py`, `tests/fakes.py`, `tests/test_cli.py`, `tests/test_constants.py`, `tests/test_dashed_line.py`, `tests/test_square.py`, `tests/test_window.py`.
|
||||
- Toolchain at that commit, run in a clean checkout of the branch: `ruff format --check .` and `ruff check .` exit 0, `mypy` reports no issues in 16 source files, `pytest` reports 29 passed, and `doxygen Doxyfile` prints no warning.
|
||||
- Language and domain: n/a (technical type)
|
||||
- Language reviewer: none (n/a)
|
||||
- Reviewer eligibility: S01 is the author of this code (the assistant wrote it for S01) and also its reviewer, which `framework/process/review-checklist-process.md` does not allow. S01 accepted this deviation in chat on 2026-10-07; it is recorded in the traceability matrix.
|
||||
- Status of this record: decided on 2026-10-08 by S01, the reviewer. The statuses and evidence are the assistant's assessment from the toolchain, an AST scan and a reading of the code, confirmed by S01.
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules, functions and variables are `snake_case` (`draw_square`, `draw_dashed_line`, `create_window`, `build_parser`). The classes `Pen`, `Window`, `TurtleUnavailableError`, `FakePen` and `FakeWindow` are `PascalCase` and the exception ends in `Error`. Constants are `UPPER_SNAKE`. ruff `N` is clean. |
|
||||
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`dash_count`, `gap_length`, `side_length`, `create_pen`); the scan finds no single-letter names. Short names: `pen`, `args`, `argv` (standard) and `tim`. `tim` in `cli.main` is the lecture's name for the turtle and nothing in the code explains it. Judgement call for the reviewer (see the action items). S01 accepted the name `tim` as the course's name on 2026-10-08. |
|
||||
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format --check .` and `ruff check .` exit 0; no `# noqa`, no `# type: ignore`. Recorded exception: `window.py` imports `turtle` inside `create_window` and `create_pen` instead of at the top of the file, which the Python conventions ask for. The reason is stated in the module docstring (importing the package must not need Tk), and no linter rule flags it. S01 accepts the exception (see the action items). S01 accepted this exception on 2026-10-08. |
|
||||
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every parameter and return is annotated (scan: none unannotated); mypy strict is clean. |
|
||||
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `window.py` raises `TurtleUnavailableError` `from` the `ImportError` (2 raises, both with a cause) and `cli.main` handles it with `parser.exit`. The drawing functions raise `ValueError` for invalid lengths and counts. No bare or broad `except`; nothing is swallowed. |
|
||||
| 6 | No mutable default arguments and no shadowed builtins | Pass | Scan of the changed files: no mutable default argument and no shadowed builtin (parameters, variables, functions, classes). |
|
||||
| 7 | Files, locks and connections are managed with context managers | N-A | No file, lock or connection is opened (scan: no `open(`). The turtle window is closed by `exitonclick`, the library's own mechanism. |
|
||||
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Optional. Every public module, class and function in `src/` has a docstring (scan: none missing), and `doxygen Doxyfile` has 0 warnings. |
|
||||
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` call (scan: 0) and no logging. `cli.main` reports a missing Tk on standard error through `parser.exit`. No secret is read. |
|
||||
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No design class diagram exists, and the tasks are plain technical tasks. The only classes are the protocols `Pen` and `Window` and the test doubles `FakePen` and `FakeWindow`; each traces to a task row (MIL-002 tasks 1 and 2, issues #9 and #10). Judgement call: a strict reading would fail this criterion. S01 confirmed `N-A` on 2026-10-08. |
|
||||
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 29 tests (26 new), named for the behaviour (for example `test_draw_square_rejects_a_side_that_is_not_positive`). They pass in file order, in reverse order and file by file, and no test imports a network module (scan). Gaps: the successful path of `create_window` and `create_pen` needs a display and has no automated test (only the missing-Tk path does), and `__main__.py` (two lines) has none. A script has run the challenges against the real `turtle` library without error. |
|
||||
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | Optional. mypy strict reports no issues in 16 source files; `Any` is not used. |
|
||||
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | N-A | Optional. No dependency changed in this milestone; see [RC-014]. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go — all mandatory criteria of `QC-PY-001` pass or are `N-A`. S01 accepted the three points that needed judgement on 2026-10-08: the imports inside `create_window` and `create_pen` as a documented exception, the name `tim` as the course's name, and `N-A` for criterion 10 because the project has no design artifacts.
|
||||
|
||||
Reviewer eligibility: S01 is the author of this code and also its reviewer, which the review process does not allow; S01 accepted this deviation in chat on 2026-10-07 and it is recorded in the traceability matrix.
|
||||
|
||||
The row of the code in the traceability matrix is updated, and issue #15 closes through the commit message when the pull request that carries it merges into `main`.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Decision (criterion 3): accept the imports inside `create_window` and `create_pen` as a documented exception to "imports at the top". Closed 2026-10-08: S01 accepted. | S01 | 2026-10-14 |
|
||||
| Decision (criterion 2): rename `tim` to `pen` in `cli.py` and in the README example, or accept it as the course's name. Closed 2026-10-08: S01 accepted. | S01 | 2026-10-14 |
|
||||
| Decision (criterion 10): confirm `N-A`, because the project has no design artifacts, or require a design class diagram. Closed 2026-10-08: S01 accepted. | S01 | 2026-10-14 |
|
||||
| After the verdict `Go`: update the traceability matrix and close issue #15. Done 2026-10-08: the matrix is updated and the commit message carries `Closes #15`. | S01 | 2026-10-14 |
|
||||
|
||||
---
|
||||
|
||||
[MIL-002]: ../../milestones/mil-002-square-and-dashed-line.md
|
||||
[QC-PY-001]: ../../../framework/qc/qc-programming-python.md
|
||||
[RC-014]: ./rc-014-code-mil-001.md
|
||||
[4d7cc33]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/4d7cc33cac2c090c663409abb1e7ab0a34d871f5
|
||||
@@ -0,0 +1,67 @@
|
||||
# Review Record: Code of Milestone 003
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-016 |
|
||||
| CrossReference | [MIL-003], [QC-PY-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [4d7cc33] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: Python source code of milestone 003 (type `PY`): the Python files that this milestone added or changed, on branch `mil-003-shapes-and-random-color` at commit c695c47
|
||||
- Checklist used: [QC-PY-001] (`QC-PY-001`, Python Source Code)
|
||||
- Milestone: [MIL-003], tasks 1 to 6 (#16 to #21); this record is the review task, issue #22
|
||||
- Scope: full review of the 8 Python files this milestone added or changed: `src/turtle_challenges/__init__.py`, `src/turtle_challenges/cli.py`, `src/turtle_challenges/colors.py`, `src/turtle_challenges/constants.py`, `src/turtle_challenges/shapes.py`, `tests/test_colors.py`, `tests/test_constants.py`, `tests/test_shapes.py`.
|
||||
- Toolchain at that commit, run in a clean checkout of the branch: `ruff format --check .` and `ruff check .` exit 0, `mypy` reports no issues in 20 source files, `pytest` reports 66 passed, and `doxygen Doxyfile` prints no warning.
|
||||
- Language and domain: n/a (technical type)
|
||||
- Language reviewer: none (n/a)
|
||||
- Reviewer eligibility: S01 is the author of this code (the assistant wrote it for S01) and also its reviewer, which `framework/process/review-checklist-process.md` does not allow. S01 accepted this deviation in chat on 2026-10-07; it is recorded in the traceability matrix.
|
||||
- Status of this record: decided on 2026-10-08 by S01, the reviewer. The statuses and evidence are the assistant's assessment from the toolchain, an AST scan and a reading of the code, confirmed by S01.
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules `colors` and `shapes`, functions `random_color`, `random_palette_color`, `draw_shape` and `draw_shapes` and the tests are `snake_case`; constants such as `COLOR_PALETTE` and `MIN_POLYGON_SIDES` are `UPPER_SNAKE`. No class is defined. ruff `N` is clean. |
|
||||
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`num_sides`, `side_length`, `random_palette_color`); no single-letter names (scan). Short names: `rng` (every docstring explains it as the random generator to draw from), `red`, `green`, `blue`, and `tim`, carried over from `cli.py` (see [RC-015]). |
|
||||
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format --check .` and `ruff check .` exit 0; no `# noqa`, no `# type: ignore`. |
|
||||
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every parameter and return is annotated (scan: none unannotated); mypy strict is clean. |
|
||||
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `draw_shape` raises `ValueError` for fewer than 3 sides or a side that is not positive; no code in this milestone catches an exception. |
|
||||
| 6 | No mutable default arguments and no shadowed builtins | Pass | Scan of the changed files: no mutable default argument and no shadowed builtin (parameters, variables, functions, classes). |
|
||||
| 7 | Files, locks and connections are managed with context managers | N-A | No file, lock or connection is opened (scan: no `open(`). |
|
||||
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Optional. Every public module and function in `src/` has a docstring (scan: none missing); `doxygen Doxyfile` has 0 warnings. |
|
||||
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` call (scan) and no logging; no secret is read. |
|
||||
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No class is defined and no design class diagram exists; the tasks are plain technical tasks (see [RC-015] criterion 10). S01 confirmed `N-A` on 2026-10-08. |
|
||||
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 66 tests (37 new), named for the behaviour: type, range and seeded repeatability of `random_color`; the turn angle and the side count for each shape from 3 to 10 sides (parametrised); the `ValueError` cases; the palette use of `draw_shapes`. They pass in file order, in reverse order and file by file; no network imports (scan). |
|
||||
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | Optional. mypy strict reports no issues in 20 source files; `Any` is not used. |
|
||||
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | N-A | Optional. No dependency changed in this milestone; see [RC-014]. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go — all mandatory criteria of `QC-PY-001` pass or are `N-A`. S01 confirmed `N-A` for criterion 10 on 2026-10-08, as in [RC-015].
|
||||
|
||||
Reviewer eligibility: S01 is the author of this code and also its reviewer, which the review process does not allow; S01 accepted this deviation in chat on 2026-10-07 and it is recorded in the traceability matrix.
|
||||
|
||||
The row of the code in the traceability matrix is updated, and issue #22 closes through the commit message when the pull request that carries it merges into `main`.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Decision (criterion 10): confirm `N-A` as in [RC-015]. Closed 2026-10-08: S01 accepted. | S01 | 2026-10-17 |
|
||||
| After the verdict `Go`: update the traceability matrix and close issue #22. Done 2026-10-08: the matrix is updated and the commit message carries `Closes #22`. | S01 | 2026-10-17 |
|
||||
|
||||
---
|
||||
|
||||
[MIL-003]: ../../milestones/mil-003-shapes-and-random-color.md
|
||||
[QC-PY-001]: ../../../framework/qc/qc-programming-python.md
|
||||
[RC-014]: ./rc-014-code-mil-001.md
|
||||
[RC-015]: ./rc-015-code-mil-002.md
|
||||
[4d7cc33]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/4d7cc33cac2c090c663409abb1e7ab0a34d871f5
|
||||
@@ -0,0 +1,68 @@
|
||||
# Review Record: Code of Milestone 004
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-017 |
|
||||
| CrossReference | [MIL-004], [QC-PY-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [4d7cc33] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: Python source code of milestone 004 (type `PY`): the Python files that this milestone added or changed, on branch `mil-004-random-walk-and-spirograph` at commit 795b9db
|
||||
- Checklist used: [QC-PY-001] (`QC-PY-001`, Python Source Code)
|
||||
- Milestone: [MIL-004], tasks 1 to 4 (#23 to #26) and the code side of task 5 (#27); this record is the review task, issue #29
|
||||
- Scope: full review of the 9 Python files this milestone added or changed: `src/turtle_challenges/__init__.py`, `src/turtle_challenges/cli.py`, `src/turtle_challenges/constants.py`, `src/turtle_challenges/random_walk.py`, `src/turtle_challenges/spirograph.py`, `tests/test_cli.py`, `tests/test_constants.py`, `tests/test_random_walk.py`, `tests/test_spirograph.py`.
|
||||
- Toolchain at that commit, run in a clean checkout of the branch: `ruff format --check .` and `ruff check .` exit 0, `mypy` reports no issues in 24 source files, `pytest` reports 95 passed, and `doxygen Doxyfile` prints no warning.
|
||||
- Language and domain: n/a (technical type)
|
||||
- Language reviewer: none (n/a)
|
||||
- Reviewer eligibility: S01 is the author of this code (the assistant wrote it for S01) and also its reviewer, which `framework/process/review-checklist-process.md` does not allow. S01 accepted this deviation in chat on 2026-10-07; it is recorded in the traceability matrix.
|
||||
- Status of this record: decided on 2026-10-08 by S01, the reviewer. The statuses and evidence are the assistant's assessment from the toolchain, an AST scan and a reading of the code, confirmed by S01.
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules `random_walk` and `spirograph`, functions `random_walk` and `draw_spirograph` and the tests are `snake_case`; constants such as `WALK_STEPS` and `SPIROGRAPH_RADIUS` are `UPPER_SNAKE`. No class is defined. ruff `N` is clean. |
|
||||
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`steps`, `distance`, `size_of_gap`, `radius`); no single-letter names (scan). `size_of_gap` and `draw_spirograph` are the course's names. Short names: `rng` (explained in each docstring), `gap` (the `--gap` option) and `tim`, still in `cli.py` (see [RC-015]). S01 accepted the name `tim` on 2026-10-08 (see [RC-015]). |
|
||||
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format --check .` and `ruff check .` exit 0; no `# noqa`, no `# type: ignore`. |
|
||||
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every parameter and return is annotated (scan: none unannotated); mypy strict is clean. |
|
||||
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `random_walk` raises `ValueError` for negative steps or a distance that is not positive, and `draw_spirograph` for a gap or radius that is not positive; no code in this milestone catches an exception. |
|
||||
| 6 | No mutable default arguments and no shadowed builtins | Pass | Scan of the changed files: no mutable default argument and no shadowed builtin (parameters, variables, functions, classes). |
|
||||
| 7 | Files, locks and connections are managed with context managers | N-A | No file, lock or connection is opened (scan: no `open(`). |
|
||||
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Optional. Every public module and function in `src/` has a docstring (scan: none missing); `doxygen Doxyfile` has 0 warnings. |
|
||||
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` call (scan) and no logging; no secret is read. |
|
||||
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No class is defined and no design class diagram exists; the tasks are plain technical tasks (see [RC-015] criterion 10). S01 confirmed `N-A` on 2026-10-08. |
|
||||
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 95 tests (29 new), named for the behaviour: seeded repeatability, step count and distance, headings drawn only from the four directions, pen size and speed set before the first move, the circle count `int(360 / size_of_gap)` including a gap of 7 and a float gap, the heading change, the `ValueError` cases, and the `--gap` option of the command line. They pass in file order, in reverse order and file by file; no network imports (scan). The real turtle window is not covered by an automated test (see [RC-015] criterion 11). |
|
||||
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | Optional. mypy strict reports no issues in 24 source files; `Any` is not used. |
|
||||
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | N-A | Optional. No dependency changed in this milestone; see [RC-014]. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go — all mandatory criteria of `QC-PY-001` pass or are `N-A`. S01 confirmed `N-A` for criterion 10 and accepted the name `tim` on 2026-10-08, as in [RC-015].
|
||||
|
||||
Reviewer eligibility: S01 is the author of this code and also its reviewer, which the review process does not allow; S01 accepted this deviation in chat on 2026-10-07 and it is recorded in the traceability matrix.
|
||||
|
||||
The row of the code in the traceability matrix is updated, and issue #29 closes through the commit message when the pull request that carries it merges into `main`.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Decision (criterion 10): confirm `N-A` as in [RC-015]. Closed 2026-10-08: S01 accepted. | S01 | 2026-10-21 |
|
||||
| Decision (criterion 2): the name `tim` in `cli.py` is decided in [RC-015]. Closed 2026-10-08: S01 accepted. | S01 | 2026-10-21 |
|
||||
| After the verdict `Go`: update the traceability matrix and close issue #29. Done 2026-10-08: the matrix is updated and the commit message carries `Closes #29`. | S01 | 2026-10-21 |
|
||||
|
||||
---
|
||||
|
||||
[MIL-004]: ../../milestones/mil-004-random-walk-and-spirograph.md
|
||||
[QC-PY-001]: ../../../framework/qc/qc-programming-python.md
|
||||
[RC-014]: ./rc-014-code-mil-001.md
|
||||
[RC-015]: ./rc-015-code-mil-002.md
|
||||
[4d7cc33]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/4d7cc33cac2c090c663409abb1e7ab0a34d871f5
|
||||
@@ -0,0 +1,63 @@
|
||||
# Review Record: Business Case success criteria
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-018 |
|
||||
| CrossReference | [BC-001], [MIL-004] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [76ac50e] |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Verdict recorded: Go-with-conditions | [54f60a9] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: the delivered repository, branch `mil-004-random-walk-and-spirograph` at commit 575df4d, against the Success Criteria of [BC-001]
|
||||
- Checklist used: the seven rows of the `## Success Criteria` table of [BC-001]; no `QC-*` checklist covers this check, the table is the checklist
|
||||
- Milestone: [MIL-004], Go / No-Go criterion 6 and task 6 (issue #28)
|
||||
- Scope: all seven success criteria
|
||||
- Language and domain: n/a (technical record)
|
||||
- Language reviewer: none (n/a)
|
||||
- Method: a fresh clone of the branch into an empty folder, then the README's Windows PowerShell steps, run on 2026-10-08 on the author's Windows machine; the real command started from that clone; read-only calls to the Gitea and GitHub APIs for continuous integration, settings and pull requests.
|
||||
- Reviewer eligibility: S01 is the author and the reviewer; S01 accepted that deviation in chat on 2026-10-07 (see the traceability matrix).
|
||||
- Status of this record: decided on 2026-10-08 by S01, the reviewer, who recorded the verdict Go-with-conditions on the assistant's evidence. The statuses and evidence are the assistant's, from the runs described above.
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Challenges are runnable. Target: 5 of 5 challenges start from the command line and draw to completion. Measure: S01 runs each on Windows PowerShell and records the result in the review of the milestone. | Pass | On the Windows machine, in PowerShell, from the fresh clone described under criterion 5: (a) the real command `turtle-challenges <name>` was started five times and each time a window titled "Turtle Challenges" opened (square, dashed-line, shapes, random-walk, spirograph). (b) Each challenge was run through the command line code (`cli.main`) with real Tk, closing the window where the command would wait for a click, and each reached that point and returned 0: square 1.4 s, dashed-line 3.2 s, shapes 14.0 s, random-walk 9.8 s, spirograph 4.2 s (1.9 s with `--gap 10`). Not covered: the click that closes the window, and S01's own run, which the measure names. |
|
||||
| 2 | Automated tests pass. Target: 0 failures; at least one test for every public function. Measure: `python -m pytest` exits 0 locally and in continuous integration. | Pass | Fresh clone, README steps: `python -m pytest` printed `95 passed`, exit 0. Every public function and class in `src/` is referenced by at least one test (scan of 16 names: none missing); `create_window` and `create_pen` are tested only for the missing-Tk path. Continuous integration: the workflow `CI` ran on the host for each pull request head (runs 217, 218, 219, 220, 227 and 228, all `success`); the latest, run 228 on `575df4d`, passed all nine steps (checkout, Python 3.13, Doxygen, install, lint, formatting, types, tests, documentation). |
|
||||
| 3 | Runtime dependencies. Target: 0. Measure: `dependencies = []` in `pyproject.toml`. | Pass | `dependencies = []` in `pyproject.toml` (read with `tomllib`), and `python -m pip show turtle-challenges` in the fresh clone prints an empty `Requires:`. |
|
||||
| 4 | Source documentation builds. Target: 0 Doxygen warnings. Measure: `doxygen Doxyfile` output. | Pass | `doxygen Doxyfile` (Doxygen 1.15.0) in the fresh clone: exit 0, no output, `build/html/index.html` generated. The same build passed on the host in run 228 (step "Build the source documentation"), where warnings fail the build. |
|
||||
| 5 | Set-up is reproducible. Target: A fresh clone runs the challenges and the tests following only the README on Windows PowerShell. Measure: S01 follows the README from an empty folder. | Fail | Not met as written yet. The README tells a reader to clone `https://github.com/Tirsvad-Udemy-100-days-of-code/018-turtle.git`, which holds only the initial commit because the code is still in the four open pull requests, so that clone has no project. With the Gitea branch URL substituted for that one line, every other README step worked in an empty folder under PowerShell: `py -3.13 -m venv .venv` (Python 3.13.14), `Activate.ps1`, `python -m pip install --upgrade pip` (26.2.1), `python -m pip install -e ".[dev]"` (exit 0), `python -m pytest` (95 passed), `ruff check .`, `ruff format --check .` and `mypy` (all exit 0), `turtle-challenges --help` (exit 0) and `doxygen Doxyfile` (0 warnings). The `framework/` submodule was not fetched and nothing needed it. The measure names S01 following the README, which has not happened. |
|
||||
| 6 | Repository is presented. Target: Non-empty description and at least 5 topics on each host. Measure: Repository settings page of each host. | Pass | Description and topics read from the host APIs: Gitea has a description ("Turtle graphics exercises from Udemy's 100 Days of Code Python bootcamp (day 18) ...") and 11 topics; GitHub has the same description and 11 topics; the target is a non-empty description and at least 5 topics. |
|
||||
| 7 | Work is traceable. Target: 100 % of pull requests carry one `Closes #N` line per completed issue. Measure: Pull request descriptions. | Pass | Pull requests #30, #31, #32 and #33: each description has its `Closes #N` lines one per line and no comma form (#30: 1 to 6; #31: 9 to 14; #32: 16 to 21; #33: 23 to 26 and 8, 15, 22, 29). Every issue those lines name is a task completed by the pull request. Issue #7 was completed on the host without a commit, so #30 only references it and it was closed by hand; #27 is only partly done, so #33 only references it. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go-with-conditions — six of the seven success criteria of [BC-001] are met with evidence (1 to 4, 6 and 7). Criterion 5 is not met as the README is written, because its clone URL points at a GitHub repository that does not hold the code until the pull requests are merged and GitHub has them. S01 recorded this verdict on 2026-10-08.
|
||||
|
||||
The conditions are the first three action items below. Until they are closed, Go / No-Go criterion 6 of [MIL-004] ("every criterion met") is not met and issue #28 stays open. A new record that names this one re-checks the conditions and ends the matter with a `Go`. Nothing is broken in the project itself: the same README steps work from the Gitea branch. No date moved, so the Project Plan needs no schedule change.
|
||||
|
||||
Reviewer eligibility: S01 is the author and the reviewer, which the review process does not allow; S01 accepted that deviation in chat on 2026-10-07 and it is recorded in the traceability matrix.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Condition (criterion 5): after pull requests #30 to #33 are merged and GitHub has `main` with the code, repeat the README steps from an empty folder with the README's own clone URL and record the result in a new record that names this one. | S01 | 2026-10-21 |
|
||||
| Condition (criterion 1): run each of the five commands once on Windows PowerShell, click the window to close it, and record the result, or accept this record's run as the measure. | S01 | 2026-10-21 |
|
||||
| Decide how `main` reaches GitHub (a push mirror from Gitea, or a push by hand). Without it a visitor from GitHub clones an empty project. | S01 | 2026-10-21 |
|
||||
| Optional: the Project Plan's open issue about a continuous integration runner is answered (a runner exists and run 228 passed); updating it needs a new Version History row on `PP-001`. | S01 | 2026-10-21 |
|
||||
|
||||
---
|
||||
|
||||
[BC-001]: ../../business-case.md
|
||||
[MIL-004]: ../../milestones/mil-004-random-walk-and-spirograph.md
|
||||
[76ac50e]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/76ac50e5910f74738251994063d191b55f1a1d28
|
||||
[54f60a9]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/54f60a951a3a6d3c6c78de373e3b76720e48639c
|
||||
@@ -9,7 +9,8 @@
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [4b3391b] |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Added the row for the check of the success criteria of [BC-001], with [RC-018] | [88aee40] |
|
||||
| 2026-10-08 | Proposed | Jens Tirsvad Nielsen | S01 | Recorded the verdict Go-with-conditions of [RC-018] | [54f60a9] |
|
||||
|
||||
---
|
||||
|
||||
@@ -31,11 +32,18 @@ updated whenever an artifact instance is created or reviewed.
|
||||
| [MIL-002] | MIL | en | it | [BC-001], [MIL-001] | [MIL-003] | [RC-011] (draft re-review of [RC-004], Go) |
|
||||
| [MIL-003] | MIL | en | it | [BC-001], [MIL-002] | [MIL-004] | [RC-012] (draft re-review of [RC-005], Go) |
|
||||
| [MIL-004] | MIL | en | it | [BC-001], [MIL-003] | - | [RC-013] (draft re-review of [RC-006], Go) |
|
||||
| Python source code of milestone 001 (`src/`, `tests/`, branch `mil-001-project-foundation`) | PY | - | - | [MIL-001] | - | [RC-014] (Go) |
|
||||
| Python source code of milestone 002 (`src/`, `tests/`, branch `mil-002-square-and-dashed-line`) | PY | - | - | [MIL-002] | - | [RC-015] (Go) |
|
||||
| Python source code of milestone 003 (`src/`, `tests/`, branch `mil-003-shapes-and-random-color`) | PY | - | - | [MIL-003] | - | [RC-016] (Go) |
|
||||
| Python source code of milestone 004 (`src/`, `tests/`, branch `mil-004-random-walk-and-spirograph`) | PY | - | - | [MIL-004] | - | [RC-017] (Go) |
|
||||
| Success criteria of [BC-001], checked against the delivered repository for the gate of [MIL-004] | BC | en | it | [BC-001], [MIL-004] | - | [RC-018] (Go-with-conditions) |
|
||||
|
||||
## Coverage Notes
|
||||
|
||||
- `-` in Upstream means foundational ([SA-001] is the foundation of the stakeholder IDs); in Downstream it means nothing is built on it yet ([MIL-004] is the last gateway; the source code under `src/` is not an instance of this matrix); in Last Reviewed it means no `RC-*` exists.
|
||||
- [PP-001] has no `RC-*` because the Project Plan has no QC checklist; the Product Owner accepts it directly.
|
||||
- `-` in Upstream means foundational ([SA-001] is the foundation of the stakeholder IDs); in Downstream it means nothing is built on it yet ([MIL-004] is the last gateway; the source code is listed per milestone in the `PY` rows); in Last Reviewed it means no `RC-*` exists.
|
||||
- [PP-001] has no `RC-*` because the Project Plan has no QC checklist; the Product Owner accepted it directly on 2026-10-08.
|
||||
- The `PY` rows list the Python code each milestone added or changed, reviewed against `QC-PY-001`. Their records, [RC-014] to [RC-017], ended in `Go` on 2026-10-08; a `-` in Language and Domain is right for source code, a technical type.
|
||||
- [RC-018] checks the seven success criteria of [BC-001] against the delivered repository, as the evidence for the gate of [MIL-004]. It ended in Go-with-conditions on 2026-10-08: criterion 5 is not met until the code is on `main` and on GitHub, and its first three action items are the conditions. A later record that names it re-checks them.
|
||||
- Each artifact lists its latest review record: the delta re-review ([RC-008] to [RC-013]) of the first review ([RC-001] to [RC-006]), or [RC-007] for [DICT-001]. All of them ended in `Go` on 2026-10-07.
|
||||
- S01 is the author and also the reviewer of every artifact. The review process does not allow this; S01 accepted the deviation in chat on 2026-10-07 because the project has one person and no governance document (`GOV`) exists.
|
||||
- No instance exists yet for the types KPI, RA, BMC, BPMN, UCD, US, UC, DM, SSD, OC, SD, DCD, ERD, ADR, GOV or TRR. One of them matters for the open reviews: GOV (the rule that the reviewer is not the author).
|
||||
@@ -63,4 +71,10 @@ updated whenever an artifact instance is created or reviewed.
|
||||
[RC-011]: ./reviews/rc-011-mil-002-re-review.md
|
||||
[RC-012]: ./reviews/rc-012-mil-003-re-review.md
|
||||
[RC-013]: ./reviews/rc-013-mil-004-re-review.md
|
||||
[4b3391b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/4b3391b22626b49aca06a7c265c1d0be93d1155d
|
||||
[RC-014]: ./reviews/rc-014-code-mil-001.md
|
||||
[RC-015]: ./reviews/rc-015-code-mil-002.md
|
||||
[RC-016]: ./reviews/rc-016-code-mil-003.md
|
||||
[RC-017]: ./reviews/rc-017-code-mil-004.md
|
||||
[RC-018]: ./reviews/rc-018-business-case-success-criteria.md
|
||||
[88aee40]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/88aee4051d3fd7a915bc793c157e421f607e3034
|
||||
[54f60a9]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/018-turtle/commit/54f60a951a3a6d3c6c78de373e3b76720e48639c
|
||||
|
||||
@@ -3,15 +3,28 @@
|
||||
@brief Solutions to the turtle graphics challenges of day 18 of Udemy's
|
||||
100 Days of Code.
|
||||
|
||||
The challenges so far are @ref turtle_challenges.square.draw_square and
|
||||
@ref turtle_challenges.dashed_line.draw_dashed_line. Each takes the turtle as its
|
||||
The five challenges are @ref turtle_challenges.square.draw_square,
|
||||
@ref turtle_challenges.dashed_line.draw_dashed_line,
|
||||
@ref turtle_challenges.shapes.draw_shape,
|
||||
@ref turtle_challenges.random_walk.random_walk and
|
||||
@ref turtle_challenges.spirograph.draw_spirograph. Each takes the turtle as its
|
||||
first argument, so it can be tested without a window.
|
||||
"""
|
||||
|
||||
from turtle_challenges.colors import random_color, random_palette_color
|
||||
from turtle_challenges.dashed_line import draw_dashed_line
|
||||
from turtle_challenges.random_walk import random_walk
|
||||
from turtle_challenges.shapes import draw_shape, draw_shapes
|
||||
from turtle_challenges.spirograph import draw_spirograph
|
||||
from turtle_challenges.square import draw_square
|
||||
|
||||
__all__ = [
|
||||
"draw_dashed_line",
|
||||
"draw_shape",
|
||||
"draw_shapes",
|
||||
"draw_spirograph",
|
||||
"draw_square",
|
||||
"random_color",
|
||||
"random_palette_color",
|
||||
"random_walk",
|
||||
]
|
||||
|
||||
@@ -10,10 +10,17 @@ from functools import partial
|
||||
from turtle_challenges.constants import (
|
||||
CHALLENGE_DASHED_LINE,
|
||||
CHALLENGE_NAMES,
|
||||
CHALLENGE_RANDOM_WALK,
|
||||
CHALLENGE_SHAPES,
|
||||
CHALLENGE_SPIROGRAPH,
|
||||
CHALLENGE_SQUARE,
|
||||
SPIROGRAPH_GAP_DEGREES,
|
||||
)
|
||||
from turtle_challenges.dashed_line import draw_dashed_line
|
||||
from turtle_challenges.pen import Pen
|
||||
from turtle_challenges.random_walk import random_walk
|
||||
from turtle_challenges.shapes import draw_shapes
|
||||
from turtle_challenges.spirograph import draw_spirograph
|
||||
from turtle_challenges.square import draw_square
|
||||
from turtle_challenges.window import (
|
||||
TurtleUnavailableError,
|
||||
@@ -26,7 +33,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"""!
|
||||
@brief Build the argument parser of the command line.
|
||||
|
||||
@return A parser with the challenge name.
|
||||
@return A parser with the challenge name and the spirograph gap option.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="turtle-challenges",
|
||||
@@ -38,20 +45,31 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
choices=CHALLENGE_NAMES,
|
||||
help="the challenge to run",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gap",
|
||||
type=float,
|
||||
default=SPIROGRAPH_GAP_DEGREES,
|
||||
help="spirograph only: degrees between two circles "
|
||||
f"(default: {SPIROGRAPH_GAP_DEGREES})",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def build_challenges(pen: Pen) -> dict[str, Callable[[], None]]:
|
||||
def build_challenges(pen: Pen, gap: float) -> dict[str, Callable[[], None]]:
|
||||
"""!
|
||||
@brief Bind every challenge to a turtle.
|
||||
|
||||
@param pen The turtle that draws.
|
||||
@param gap Degrees between two circles of the spirograph.
|
||||
@return A mapping from the command line name to a function without
|
||||
arguments that draws the challenge.
|
||||
"""
|
||||
return {
|
||||
CHALLENGE_SQUARE: partial(draw_square, pen),
|
||||
CHALLENGE_DASHED_LINE: partial(draw_dashed_line, pen),
|
||||
CHALLENGE_SHAPES: partial(draw_shapes, pen),
|
||||
CHALLENGE_RANDOM_WALK: partial(random_walk, pen),
|
||||
CHALLENGE_SPIROGRAPH: partial(draw_spirograph, pen, gap),
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +87,6 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
tim = create_pen()
|
||||
except TurtleUnavailableError as error:
|
||||
parser.exit(1, f"{parser.prog}: error: {error}\n")
|
||||
build_challenges(tim)[args.challenge]()
|
||||
build_challenges(tim, args.gap)[args.challenge]()
|
||||
window.exitonclick()
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""!
|
||||
@file colors.py
|
||||
@brief Random colors for the challenges.
|
||||
|
||||
A color is either a name from the palette (challenge 3) or an RGB tuple
|
||||
(challenges 4 and 5). A tuple is the right type for an RGB value: it is
|
||||
immutable, so a color cannot change after it is made.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from turtle_challenges.constants import (
|
||||
COLOR_CHANNEL_MAX,
|
||||
COLOR_CHANNEL_MIN,
|
||||
COLOR_PALETTE,
|
||||
)
|
||||
|
||||
## Generator used when a caller passes none; tests pass a seeded one instead.
|
||||
_DEFAULT_RANDOM = random.Random()
|
||||
|
||||
|
||||
def random_color(rng: random.Random | None = None) -> tuple[int, int, int]:
|
||||
"""!
|
||||
@brief Pick a random RGB color.
|
||||
|
||||
@param rng Random generator to draw from; a shared one is used if omitted.
|
||||
@return A tuple of red, green and blue, each from 0 to 255.
|
||||
"""
|
||||
generator = rng if rng is not None else _DEFAULT_RANDOM
|
||||
red = generator.randint(COLOR_CHANNEL_MIN, COLOR_CHANNEL_MAX)
|
||||
green = generator.randint(COLOR_CHANNEL_MIN, COLOR_CHANNEL_MAX)
|
||||
blue = generator.randint(COLOR_CHANNEL_MIN, COLOR_CHANNEL_MAX)
|
||||
return (red, green, blue)
|
||||
|
||||
|
||||
def random_palette_color(rng: random.Random | None = None) -> str:
|
||||
"""!
|
||||
@brief Pick a random color name from the palette.
|
||||
|
||||
@param rng Random generator to draw from; a shared one is used if omitted.
|
||||
@return One of the names in `COLOR_PALETTE`.
|
||||
"""
|
||||
generator = rng if rng is not None else _DEFAULT_RANDOM
|
||||
return generator.choice(COLOR_PALETTE)
|
||||
@@ -14,9 +14,21 @@ WINDOW_TITLE = "Turtle Challenges"
|
||||
## colors can be given as RGB tuples from 0 to 255.
|
||||
COLOR_MODE = 255
|
||||
|
||||
## Smallest value of one color channel.
|
||||
COLOR_CHANNEL_MIN = 0
|
||||
|
||||
## Largest value of one color channel.
|
||||
COLOR_CHANNEL_MAX = COLOR_MODE
|
||||
|
||||
## Degrees in a full turn.
|
||||
FULL_TURN_DEGREES = 360
|
||||
|
||||
## Degrees in a right angle.
|
||||
RIGHT_ANGLE_DEGREES = 90
|
||||
|
||||
## Turtle speed setting for "fastest": no animation between moves.
|
||||
FASTEST_SPEED = 0
|
||||
|
||||
## Challenge 1: number of sides of a square.
|
||||
SQUARE_SIDES = 4
|
||||
|
||||
@@ -32,14 +44,65 @@ DASH_LENGTH = 10
|
||||
## Challenge 2: length of the gap after each dash, in turtle units.
|
||||
GAP_LENGTH = 10
|
||||
|
||||
## Challenge 3: fewest sides of a polygon (a triangle).
|
||||
MIN_POLYGON_SIDES = 3
|
||||
|
||||
## Challenge 3: most sides of a polygon drawn in a row (a decagon).
|
||||
MAX_POLYGON_SIDES = 10
|
||||
|
||||
## Challenge 3: length of one side of a polygon, in turtle units.
|
||||
POLYGON_SIDE_LENGTH = 100
|
||||
|
||||
## Challenge 3: named colors a polygon is drawn in.
|
||||
COLOR_PALETTE: tuple[str, ...] = (
|
||||
"CornflowerBlue",
|
||||
"DarkOrchid",
|
||||
"IndianRed",
|
||||
"DeepSkyBlue",
|
||||
"LightSeaGreen",
|
||||
"wheat",
|
||||
"SlateGray",
|
||||
"SeaGreen",
|
||||
)
|
||||
|
||||
## Challenge 4: number of steps of the random walk.
|
||||
WALK_STEPS = 200
|
||||
|
||||
## Challenge 4: distance of one step, in turtle units.
|
||||
WALK_STEP_DISTANCE = 30
|
||||
|
||||
## Challenge 4: thickness of the line, in pixels.
|
||||
WALK_PEN_SIZE = 10
|
||||
|
||||
## Challenge 4: headings of a step: east, north, west and south, in degrees.
|
||||
WALK_HEADINGS: tuple[int, ...] = (0, 90, 180, 270)
|
||||
|
||||
## Challenge 5: radius of every circle of the spirograph, in turtle units.
|
||||
SPIROGRAPH_RADIUS = 100
|
||||
|
||||
## Challenge 5: degrees the heading turns after each circle.
|
||||
SPIROGRAPH_GAP_DEGREES = 5
|
||||
|
||||
## Command line name of challenge 1.
|
||||
CHALLENGE_SQUARE = "square"
|
||||
|
||||
## Command line name of challenge 2.
|
||||
CHALLENGE_DASHED_LINE = "dashed-line"
|
||||
|
||||
## Command line name of challenge 3.
|
||||
CHALLENGE_SHAPES = "shapes"
|
||||
|
||||
## Command line name of challenge 4.
|
||||
CHALLENGE_RANDOM_WALK = "random-walk"
|
||||
|
||||
## Command line name of challenge 5.
|
||||
CHALLENGE_SPIROGRAPH = "spirograph"
|
||||
|
||||
## Names of the challenges on the command line, in the order of the course.
|
||||
CHALLENGE_NAMES: tuple[str, ...] = (
|
||||
CHALLENGE_SQUARE,
|
||||
CHALLENGE_DASHED_LINE,
|
||||
CHALLENGE_SHAPES,
|
||||
CHALLENGE_RANDOM_WALK,
|
||||
CHALLENGE_SPIROGRAPH,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""!
|
||||
@file random_walk.py
|
||||
@brief Turtle Challenge 4: generate a random walk.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from turtle_challenges.colors import random_color
|
||||
from turtle_challenges.constants import (
|
||||
FASTEST_SPEED,
|
||||
WALK_HEADINGS,
|
||||
WALK_PEN_SIZE,
|
||||
WALK_STEP_DISTANCE,
|
||||
WALK_STEPS,
|
||||
)
|
||||
from turtle_challenges.pen import Pen
|
||||
|
||||
|
||||
def random_walk(
|
||||
pen: Pen,
|
||||
steps: int = WALK_STEPS,
|
||||
distance: float = WALK_STEP_DISTANCE,
|
||||
rng: random.Random | None = None,
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Walk in random directions, changing color at every step.
|
||||
|
||||
Each step faces east, north, west or south at random and moves the same
|
||||
distance in a new random RGB color.
|
||||
|
||||
@param pen The turtle that draws.
|
||||
@param steps Number of steps to take.
|
||||
@param distance Distance of one step, in turtle units.
|
||||
@param rng Random generator for headings and colors; a shared one is used
|
||||
if omitted.
|
||||
@exception ValueError If @p steps is negative, or @p distance is not
|
||||
positive.
|
||||
"""
|
||||
if steps < 0:
|
||||
raise ValueError(f"steps must not be negative, got {steps}")
|
||||
if distance <= 0:
|
||||
raise ValueError(f"distance must be positive, got {distance}")
|
||||
generator = rng if rng is not None else random.Random()
|
||||
pen.pensize(WALK_PEN_SIZE)
|
||||
pen.speed(FASTEST_SPEED)
|
||||
for _ in range(steps):
|
||||
pen.color(random_color(generator))
|
||||
pen.setheading(generator.choice(WALK_HEADINGS))
|
||||
pen.forward(distance)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""!
|
||||
@file shapes.py
|
||||
@brief Turtle Challenge 3: draw different shapes, each in a random color.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from turtle_challenges.colors import random_palette_color
|
||||
from turtle_challenges.constants import (
|
||||
FULL_TURN_DEGREES,
|
||||
MAX_POLYGON_SIDES,
|
||||
MIN_POLYGON_SIDES,
|
||||
POLYGON_SIDE_LENGTH,
|
||||
)
|
||||
from turtle_challenges.pen import Pen
|
||||
|
||||
|
||||
def draw_shape(
|
||||
pen: Pen, num_sides: int, side_length: float = POLYGON_SIDE_LENGTH
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Draw a regular polygon.
|
||||
|
||||
The turtle turns 360 divided by the number of sides after each side, so
|
||||
five sides turn 72 degrees and the shape closes after one lap.
|
||||
|
||||
@param pen The turtle that draws.
|
||||
@param num_sides Number of sides; at least 3.
|
||||
@param side_length Length of each side, in turtle units.
|
||||
@exception ValueError If @p num_sides is below 3, or @p side_length is not
|
||||
positive.
|
||||
"""
|
||||
if num_sides < MIN_POLYGON_SIDES:
|
||||
raise ValueError(
|
||||
f"num_sides must be at least {MIN_POLYGON_SIDES}, got {num_sides}"
|
||||
)
|
||||
if side_length <= 0:
|
||||
raise ValueError(f"side_length must be positive, got {side_length}")
|
||||
angle = FULL_TURN_DEGREES / num_sides
|
||||
for _ in range(num_sides):
|
||||
pen.forward(side_length)
|
||||
pen.right(angle)
|
||||
|
||||
|
||||
def draw_shapes(pen: Pen, rng: random.Random | None = None) -> None:
|
||||
"""!
|
||||
@brief Draw a triangle, square, pentagon and so on up to a decagon.
|
||||
|
||||
Each shape gets a random color from the palette.
|
||||
|
||||
@param pen The turtle that draws.
|
||||
@param rng Random generator for the colors; a shared one is used if omitted.
|
||||
"""
|
||||
for num_sides in range(MIN_POLYGON_SIDES, MAX_POLYGON_SIDES + 1):
|
||||
pen.color(random_palette_color(rng))
|
||||
draw_shape(pen, num_sides)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""!
|
||||
@file spirograph.py
|
||||
@brief Turtle Challenge 5: draw a spirograph.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from turtle_challenges.colors import random_color
|
||||
from turtle_challenges.constants import (
|
||||
FASTEST_SPEED,
|
||||
FULL_TURN_DEGREES,
|
||||
SPIROGRAPH_GAP_DEGREES,
|
||||
SPIROGRAPH_RADIUS,
|
||||
)
|
||||
from turtle_challenges.pen import Pen
|
||||
|
||||
|
||||
def draw_spirograph(
|
||||
pen: Pen,
|
||||
size_of_gap: float = SPIROGRAPH_GAP_DEGREES,
|
||||
radius: float = SPIROGRAPH_RADIUS,
|
||||
rng: random.Random | None = None,
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Draw overlapping circles, turning a little after each one.
|
||||
|
||||
After every circle the heading grows by @p size_of_gap, so the circles
|
||||
overlap into a spirograph once the heading has made a full turn. The number
|
||||
of circles is 360 divided by the gap, rounded down: `range` takes only an
|
||||
integer, and a float such as 72.0 would raise a `TypeError`.
|
||||
|
||||
@param pen The turtle that draws.
|
||||
@param size_of_gap Degrees between two circles; must be positive.
|
||||
@param radius Radius of every circle, in turtle units.
|
||||
@param rng Random generator for the colors; a shared one is used if
|
||||
omitted.
|
||||
@exception ValueError If @p size_of_gap or @p radius is not positive.
|
||||
"""
|
||||
if size_of_gap <= 0:
|
||||
raise ValueError(f"size_of_gap must be positive, got {size_of_gap}")
|
||||
if radius <= 0:
|
||||
raise ValueError(f"radius must be positive, got {radius}")
|
||||
pen.speed(FASTEST_SPEED)
|
||||
for _ in range(int(FULL_TURN_DEGREES / size_of_gap)):
|
||||
pen.color(random_color(rng))
|
||||
pen.circle(radius)
|
||||
pen.setheading(pen.heading() + size_of_gap)
|
||||
+19
-1
@@ -5,6 +5,7 @@ from turtle_challenges import cli
|
||||
from turtle_challenges.constants import (
|
||||
CHALLENGE_NAMES,
|
||||
DASH_COUNT,
|
||||
SPIROGRAPH_GAP_DEGREES,
|
||||
SQUARE_SIDES,
|
||||
)
|
||||
from turtle_challenges.pen import Pen, Window
|
||||
@@ -29,7 +30,7 @@ def fake_pen_and_window(monkeypatch: pytest.MonkeyPatch) -> tuple[FakePen, FakeW
|
||||
|
||||
|
||||
def test_every_challenge_name_has_a_challenge() -> None:
|
||||
challenges = cli.build_challenges(FakePen())
|
||||
challenges = cli.build_challenges(FakePen(), SPIROGRAPH_GAP_DEGREES)
|
||||
|
||||
assert set(challenges) == set(CHALLENGE_NAMES)
|
||||
|
||||
@@ -39,6 +40,13 @@ def test_parser_accepts_every_challenge_name(name: str) -> None:
|
||||
args = cli.build_parser().parse_args([name])
|
||||
|
||||
assert args.challenge == name
|
||||
assert args.gap == SPIROGRAPH_GAP_DEGREES
|
||||
|
||||
|
||||
def test_parser_reads_the_spirograph_gap() -> None:
|
||||
args = cli.build_parser().parse_args(["spirograph", "--gap", "7.5"])
|
||||
|
||||
assert args.gap == 7.5
|
||||
|
||||
|
||||
def test_parser_rejects_an_unknown_challenge(
|
||||
@@ -82,6 +90,16 @@ def test_main_runs_the_dashed_line(
|
||||
assert len(pen.args_of("forward")) == 2 * DASH_COUNT
|
||||
|
||||
|
||||
def test_main_passes_the_gap_to_the_spirograph(
|
||||
fake_pen_and_window: tuple[FakePen, FakeWindow],
|
||||
) -> None:
|
||||
pen, _ = fake_pen_and_window
|
||||
|
||||
cli.main(["spirograph", "--gap", "90"])
|
||||
|
||||
assert len(pen.args_of("circle")) == 4
|
||||
|
||||
|
||||
def test_main_reports_a_missing_turtle_and_exits_with_status_1(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import random
|
||||
|
||||
from turtle_challenges.colors import random_color, random_palette_color
|
||||
from turtle_challenges.constants import (
|
||||
COLOR_CHANNEL_MAX,
|
||||
COLOR_CHANNEL_MIN,
|
||||
COLOR_PALETTE,
|
||||
)
|
||||
|
||||
|
||||
def test_random_color_is_a_tuple_of_three_integers() -> None:
|
||||
color = random_color(random.Random(1))
|
||||
|
||||
assert isinstance(color, tuple)
|
||||
assert len(color) == 3
|
||||
assert all(isinstance(channel, int) for channel in color)
|
||||
|
||||
|
||||
def test_random_color_channels_stay_in_range() -> None:
|
||||
rng = random.Random(2)
|
||||
|
||||
for _ in range(500):
|
||||
assert all(
|
||||
COLOR_CHANNEL_MIN <= channel <= COLOR_CHANNEL_MAX
|
||||
for channel in random_color(rng)
|
||||
)
|
||||
|
||||
|
||||
def test_random_color_is_repeatable_with_the_same_seed() -> None:
|
||||
first = [random_color(random.Random(7)) for _ in range(3)]
|
||||
second = [random_color(random.Random(7)) for _ in range(3)]
|
||||
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_random_color_varies_between_calls() -> None:
|
||||
rng = random.Random(3)
|
||||
|
||||
assert len({random_color(rng) for _ in range(20)}) > 1
|
||||
|
||||
|
||||
def test_random_color_works_without_a_generator() -> None:
|
||||
assert len(random_color()) == 3
|
||||
|
||||
|
||||
def test_random_palette_color_comes_from_the_palette() -> None:
|
||||
rng = random.Random(4)
|
||||
|
||||
assert {random_palette_color(rng) for _ in range(100)} <= set(COLOR_PALETTE)
|
||||
|
||||
|
||||
def test_random_palette_color_works_without_a_generator() -> None:
|
||||
assert random_palette_color() in COLOR_PALETTE
|
||||
@@ -1,5 +1,23 @@
|
||||
from turtle_challenges import constants
|
||||
|
||||
|
||||
def test_color_mode_matches_the_largest_channel_value() -> None:
|
||||
assert constants.COLOR_MODE == constants.COLOR_CHANNEL_MAX == 255
|
||||
|
||||
|
||||
def test_challenge_names_are_unique() -> None:
|
||||
assert len(set(constants.CHALLENGE_NAMES)) == len(constants.CHALLENGE_NAMES)
|
||||
|
||||
|
||||
def test_walk_headings_are_the_four_compass_directions() -> None:
|
||||
assert constants.WALK_HEADINGS == (0, 90, 180, 270)
|
||||
|
||||
|
||||
def test_palette_has_only_non_empty_names() -> None:
|
||||
assert constants.COLOR_PALETTE
|
||||
assert all(name for name in constants.COLOR_PALETTE)
|
||||
|
||||
|
||||
def test_polygon_range_runs_from_triangle_to_decagon() -> None:
|
||||
assert constants.MIN_POLYGON_SIDES == 3
|
||||
assert constants.MAX_POLYGON_SIDES == 10
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.fakes import FakePen
|
||||
from turtle_challenges.constants import (
|
||||
COLOR_CHANNEL_MAX,
|
||||
COLOR_CHANNEL_MIN,
|
||||
FASTEST_SPEED,
|
||||
WALK_HEADINGS,
|
||||
WALK_PEN_SIZE,
|
||||
WALK_STEP_DISTANCE,
|
||||
WALK_STEPS,
|
||||
)
|
||||
from turtle_challenges.random_walk import random_walk
|
||||
|
||||
|
||||
def test_random_walk_sets_pen_size_and_speed_before_the_first_move() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, rng=random.Random(1))
|
||||
|
||||
assert pen.calls[0] == ("pensize", (WALK_PEN_SIZE,))
|
||||
assert pen.calls[1] == ("speed", (FASTEST_SPEED,))
|
||||
|
||||
|
||||
def test_random_walk_takes_the_default_number_of_equal_steps() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, rng=random.Random(2))
|
||||
|
||||
assert pen.args_of("forward") == [(WALK_STEP_DISTANCE,)] * WALK_STEPS
|
||||
|
||||
|
||||
def test_random_walk_faces_only_the_four_headings() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, rng=random.Random(3))
|
||||
|
||||
headings = {args[0] for args in pen.args_of("setheading")}
|
||||
assert headings <= set(WALK_HEADINGS)
|
||||
assert len(headings) > 1
|
||||
|
||||
|
||||
def test_random_walk_gives_every_step_an_rgb_color() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, steps=30, rng=random.Random(4))
|
||||
|
||||
colors = [args[0] for args in pen.args_of("color")]
|
||||
assert len(colors) == 30
|
||||
for color in colors:
|
||||
assert isinstance(color, tuple)
|
||||
assert len(color) == 3
|
||||
assert all(
|
||||
COLOR_CHANNEL_MIN <= channel <= COLOR_CHANNEL_MAX for channel in color
|
||||
)
|
||||
|
||||
|
||||
def test_random_walk_picks_color_and_heading_before_each_move() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, steps=2, rng=random.Random(5))
|
||||
|
||||
assert pen.names()[2:] == ["color", "setheading", "forward"] * 2
|
||||
|
||||
|
||||
def test_random_walk_is_repeatable_with_the_same_seed() -> None:
|
||||
first = FakePen()
|
||||
second = FakePen()
|
||||
|
||||
random_walk(first, steps=25, rng=random.Random(8))
|
||||
random_walk(second, steps=25, rng=random.Random(8))
|
||||
|
||||
assert first.calls == second.calls
|
||||
|
||||
|
||||
def test_random_walk_with_no_steps_only_sets_up_the_pen() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, steps=0)
|
||||
|
||||
assert pen.names() == ["pensize", "speed"]
|
||||
|
||||
|
||||
def test_random_walk_works_without_a_generator() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
random_walk(pen, steps=3)
|
||||
|
||||
assert len(pen.args_of("forward")) == 3
|
||||
|
||||
|
||||
def test_random_walk_rejects_negative_steps() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
with pytest.raises(ValueError, match="steps"):
|
||||
random_walk(pen, steps=-1)
|
||||
|
||||
assert pen.calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("distance", [0, -3])
|
||||
def test_random_walk_rejects_a_distance_that_is_not_positive(distance: float) -> None:
|
||||
pen = FakePen()
|
||||
|
||||
with pytest.raises(ValueError, match="distance"):
|
||||
random_walk(pen, distance=distance)
|
||||
|
||||
assert pen.calls == []
|
||||
@@ -0,0 +1,94 @@
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.fakes import FakePen
|
||||
from turtle_challenges.constants import (
|
||||
COLOR_PALETTE,
|
||||
FULL_TURN_DEGREES,
|
||||
MAX_POLYGON_SIDES,
|
||||
MIN_POLYGON_SIDES,
|
||||
POLYGON_SIDE_LENGTH,
|
||||
)
|
||||
from turtle_challenges.shapes import draw_shape, draw_shapes
|
||||
|
||||
SIDE_COUNTS = range(MIN_POLYGON_SIDES, MAX_POLYGON_SIDES + 1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_sides", SIDE_COUNTS)
|
||||
def test_draw_shape_draws_every_side_at_the_default_length(num_sides: int) -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shape(pen, num_sides)
|
||||
|
||||
assert pen.args_of("forward") == [(POLYGON_SIDE_LENGTH,)] * num_sides
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_sides", SIDE_COUNTS)
|
||||
def test_draw_shape_turns_a_full_lap_in_equal_steps(num_sides: int) -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shape(pen, num_sides)
|
||||
|
||||
expected_angle = FULL_TURN_DEGREES / num_sides
|
||||
assert pen.args_of("right") == [(expected_angle,)] * num_sides
|
||||
|
||||
|
||||
def test_draw_shape_turns_72_degrees_for_a_pentagon() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shape(pen, 5)
|
||||
|
||||
assert pen.args_of("right")[0] == (72,)
|
||||
|
||||
|
||||
def test_draw_shape_uses_the_given_side_length() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shape(pen, 3, side_length=25)
|
||||
|
||||
assert pen.args_of("forward") == [(25,)] * 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_sides", [-1, 0, 1, 2])
|
||||
def test_draw_shape_rejects_fewer_than_three_sides(num_sides: int) -> None:
|
||||
pen = FakePen()
|
||||
|
||||
with pytest.raises(ValueError, match="num_sides"):
|
||||
draw_shape(pen, num_sides)
|
||||
|
||||
assert pen.calls == []
|
||||
|
||||
|
||||
def test_draw_shape_rejects_a_side_that_is_not_positive() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
with pytest.raises(ValueError, match="side_length"):
|
||||
draw_shape(pen, 4, side_length=0)
|
||||
|
||||
|
||||
def test_draw_shapes_draws_a_triangle_up_to_a_decagon() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shapes(pen, random.Random(5))
|
||||
|
||||
assert len(pen.args_of("forward")) == sum(SIDE_COUNTS)
|
||||
assert len(pen.args_of("color")) == len(SIDE_COUNTS)
|
||||
|
||||
|
||||
def test_draw_shapes_sets_a_palette_color_before_each_shape() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shapes(pen, random.Random(6))
|
||||
|
||||
colors = [args[0] for args in pen.args_of("color")]
|
||||
assert all(color in COLOR_PALETTE for color in colors)
|
||||
assert pen.names()[0] == "color"
|
||||
|
||||
|
||||
def test_draw_shapes_works_without_a_generator() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_shapes(pen)
|
||||
|
||||
assert len(pen.args_of("color")) == len(SIDE_COUNTS)
|
||||
@@ -0,0 +1,111 @@
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.fakes import FakePen
|
||||
from turtle_challenges.constants import (
|
||||
FASTEST_SPEED,
|
||||
FULL_TURN_DEGREES,
|
||||
SPIROGRAPH_GAP_DEGREES,
|
||||
SPIROGRAPH_RADIUS,
|
||||
)
|
||||
from turtle_challenges.spirograph import draw_spirograph
|
||||
|
||||
|
||||
def test_draw_spirograph_draws_one_circle_per_gap_in_a_full_turn() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, rng=random.Random(1))
|
||||
|
||||
expected = FULL_TURN_DEGREES // SPIROGRAPH_GAP_DEGREES
|
||||
assert pen.args_of("circle") == [(SPIROGRAPH_RADIUS,)] * expected
|
||||
|
||||
|
||||
def test_draw_spirograph_turns_by_the_gap_after_each_circle() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=90, rng=random.Random(2))
|
||||
|
||||
assert pen.args_of("setheading") == [(90,), (180,), (270,), (360,)]
|
||||
|
||||
|
||||
def test_draw_spirograph_sets_a_color_before_each_circle() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=120, rng=random.Random(3))
|
||||
|
||||
assert pen.names()[1:] == ["color", "circle", "setheading"] * 3
|
||||
|
||||
|
||||
def test_draw_spirograph_gives_every_circle_an_rgb_color() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=30, rng=random.Random(4))
|
||||
|
||||
for (color,) in pen.args_of("color"):
|
||||
assert isinstance(color, tuple)
|
||||
assert len(color) == 3
|
||||
|
||||
|
||||
def test_draw_spirograph_sets_the_fastest_speed_first() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=180)
|
||||
|
||||
assert pen.calls[0] == ("speed", (FASTEST_SPEED,))
|
||||
|
||||
|
||||
def test_draw_spirograph_rounds_the_circle_count_down_for_an_uneven_gap() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=7)
|
||||
|
||||
assert len(pen.args_of("circle")) == 51
|
||||
|
||||
|
||||
def test_draw_spirograph_accepts_a_float_gap_without_a_type_error() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=2.5)
|
||||
|
||||
assert len(pen.args_of("circle")) == 144
|
||||
|
||||
|
||||
def test_draw_spirograph_uses_the_given_radius() -> None:
|
||||
pen = FakePen()
|
||||
|
||||
draw_spirograph(pen, size_of_gap=180, radius=42)
|
||||
|
||||
assert pen.args_of("circle") == [(42,), (42,)]
|
||||
|
||||
|
||||
def test_draw_spirograph_is_repeatable_with_the_same_seed() -> None:
|
||||
first = FakePen()
|
||||
second = FakePen()
|
||||
|
||||
draw_spirograph(first, size_of_gap=45, rng=random.Random(9))
|
||||
draw_spirograph(second, size_of_gap=45, rng=random.Random(9))
|
||||
|
||||
assert first.calls == second.calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size_of_gap", [0, -5])
|
||||
def test_draw_spirograph_rejects_a_gap_that_is_not_positive(
|
||||
size_of_gap: float,
|
||||
) -> None:
|
||||
pen = FakePen()
|
||||
|
||||
with pytest.raises(ValueError, match="size_of_gap"):
|
||||
draw_spirograph(pen, size_of_gap=size_of_gap)
|
||||
|
||||
assert pen.calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("radius", [0, -1])
|
||||
def test_draw_spirograph_rejects_a_radius_that_is_not_positive(radius: float) -> None:
|
||||
pen = FakePen()
|
||||
|
||||
with pytest.raises(ValueError, match="radius"):
|
||||
draw_spirograph(pen, radius=radius)
|
||||
|
||||
assert pen.calls == []
|
||||
Reference in New Issue
Block a user