Author SHA1 Message Date
TirsvadandClaude Sonnet 5.5 5ebf05c735 Resolve pending commit links for MIL-004 acceptance
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 00:00:07 +08:00
Tirsvad 267e34fc9f Accept MIL-004 and review the source code
Review MIL-004 (RC-011, Go) and the Python source against QC-PY-001
(RC-012, Go) and accept MIL-004. Update the Traceability Matrix.

Task: MIL-004#3
Closes #19
2026-10-04 23:59:32 +08:00
Tirsvad 4abf984382 Fix lint, format and typing findings and pin dev tools
Order imports, format with ruff, make the account key constants Final
so mypy --strict accepts them, and type the test helpers. Configure
ruff and mypy in pyproject.toml and pin the dev tool versions. All
modules, classes and functions already carry Doxygen comments;
doxygen Doxyfile builds without warnings.

Task: MIL-004#2
Closes #18
2026-10-04 23:59:15 +08:00
Tirsvad 6e52a242fa Add README with set-up, run, test and Doxygen instructions
Describe the game, the .venv set-up with pip upgrade, running the game
and tests, the code checks, the Doxygen build and the layout. Verified
by following it in a clean copy of the tree.

Task: MIL-004#1
Task: MIL-004#4
Closes #17
Closes #20
2026-10-04 23:58:22 +08:00
Tirsvad cbdcba0a9f Merge pull request 'MIL-003 Console game: game loop, entry point and accepted use case' (#26) from mil-003-game-loop into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 3s
Reviewed-on: #26
2026-10-04 17:50:34 +02:00
TirsvadandClaude Sonnet 5.5 5e4dd48e3e Resolve pending commit links for accepted use case artifacts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:49:52 +08:00
Tirsvad 0ada1588ee Add the console game loop and entry point
Add show_round, ask_choice and play_game with injectable input and
output, and make python -m higher_lower play a game. Each account is
shown once per game; a wrong guess or running out of accounts ends it.
Invalid input is re-asked. Ctrl+C and Ctrl+D leave with a goodbye.
Cover the loop with scripted tests (36 passing).

Task: MIL-003#1
Task: MIL-003#2
Task: MIL-003#3
Task: MIL-003#4
Task: MIL-003#5
Closes #12
Closes #13
Closes #14
Closes #15
Closes #16
2026-10-04 23:49:09 +08:00
Tirsvad c7164feab1 Accept UCD-001, US-001, UC-001 and SSD-001 after review
Review the use case chain for Play a Game (RC-007 to RC-010, all Go)
and accept the four documents. Update the Traceability Matrix.
2026-10-04 23:48:42 +08:00
Tirsvad 462deba2a0 Merge pull request 'Accept MIL-003 and add the Play a Game use case' (#25) from mil-003-console-game into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #25
2026-10-04 17:43:31 +02:00
TirsvadandClaude Sonnet 5.5 0a74e22a43 Resolve pending commit links for MIL-003 acceptance
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:42:10 +08:00
Tirsvad 923dde7dc9 Accept MIL-003 and add the Play a Game use case
Create the use case chain for the game loop task: Use Case Diagram
UCD-001, User Stories US-001 (two stories), Use Case UC-001 and System
Sequence Diagram SSD-001, all Proposed. Link them from MIL-003.

Review MIL-003 (RC-006, Go) and accept it. Add the new artifacts to the
Traceability Matrix and register UCD, US, UC and SSD in the artifact
registry.

Refs #13
2026-10-04 23:41:35 +08:00
Tirsvad 64e3f04c6a Merge pull request 'MIL-002 Game logic: data, art, constants and pure functions' (#24) from mil-002-game-logic into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #24
2026-10-04 17:36:11 +02:00
TirsvadandClaude Sonnet 5.5 037900df94 Resolve pending commit links for MIL-002 acceptance
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 23:35:18 +08:00
Tirsvad ee58fcdc1c Add game data, art, constants and game logic
Add the assignment data set (50 accounts), the logo and vs art, the
constants module, and the pure functions get_random_account,
pick_pair, format_data and check_answer, all with Doxygen comments.
pick_pair never returns the same account twice; check_answer accepts
either choice on a tie. Cover them with pytest tests (22 passing).

The art is built from concatenated raw lines so the backslashes survive
and Doxygen does not read the backticks as markup.

Accept MIL-002 after review RC-005 and add it to the Traceability
Matrix.

Task: MIL-002#1
Task: MIL-002#2
Task: MIL-002#3
Task: MIL-002#4
Task: MIL-002#5
Task: MIL-002#6
Closes #6
Closes #7
Closes #8
Closes #9
Closes #10
Closes #11
2026-10-04 23:34:48 +08:00
Tirsvad b1f4116bc1 Merge pull request 'MIL-001 Project setup: pyproject, skeleton and Doxyfile' (#23) from mil-001-project-setup into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 7s
Reviewed-on: #23
2026-10-04 17:30:02 +02:00
Tirsvad 115e543ee3 Add project configuration, skeleton and Doxyfile
Add pyproject.toml (Python >=3.13, no runtime dependencies, pytest as a
dev extra), the src/higher_lower package with an empty constants module
and an entry-point stub, a smoke test, and the Doxyfile for the Doxygen
comments in the source. Ignore the Doxygen output in .gitignore.

Verified in a fresh .venv: pip install -e ".[dev]", pytest (2 passed),
python -m higher_lower and doxygen Doxyfile without warnings.

Task: MIL-001#1
Task: MIL-001#2
Task: MIL-001#3
Task: MIL-001#4
Closes #1
Closes #2
Closes #3
Closes #4
2026-10-04 23:29:03 +08:00
Tirsvad 4b1228fec0 Merge pull request 'Add inception baseline and milestone plan for Higher Lower' (#22) from g1-inception-baseline into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #22
2026-10-04 17:24:33 +02:00
Tirsvad 2782f29c66 Merge branch 'main' into g1-inception-baseline 2026-10-04 17:24:20 +02:00
Tirsvad a999fcda34 Merge pull request 'G1 inception baseline' (#21) from g1-inception-baseline into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #21
2026-10-04 17:15:12 +02:00
32 changed files with 4721 additions and 15 deletions
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
# Doxygen output
docs/doxygen/
+2967
View File
File diff suppressed because it is too large Load Diff
+99 -1
View File
@@ -1,2 +1,100 @@
# 014-higher_lower # Higher Lower
A console game from Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*
(day 14). Two Instagram accounts are shown. Guess which one has more followers.
Every right guess adds a point and the winner stays for the next round. A wrong
guess ends the game.
```
Compare A: Cristiano Ronaldo, a Footballer, from Portugal.
_ __
| | / /____
| | / / ___/
| |/ (__ )
|___/____(_)
Against B: Ariana Grande, a Musician and actress, from United States.
Who has more followers? Type 'A' or 'B':
```
- Python 3.13 or newer, no runtime dependencies.
- The function names follow the assignment: `format_data`, `check_answer`.
- Each account is shown once per game; if you get through all 50, you win.
- Follower counts are in millions and come from the assignment's data set.
## Requirements
- [Python](https://www.python.org/downloads/) 3.13 or newer
- Optional: [Doxygen](https://www.doxygen.nl/) to build the source documentation
## Set up
Create a local virtual environment in `.venv`, activate it, and upgrade `pip`.
Windows (PowerShell):
```powershell
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
```
Windows (Git Bash), Linux and macOS:
```bash
python -m venv .venv
source .venv/Scripts/activate # Linux/macOS: source .venv/bin/activate
python -m pip install --upgrade pip
```
The game itself needs nothing more. To run the tests and the code checks, install the
development tools:
```bash
python -m pip install -e ".[dev]"
```
## Run the game
```bash
python -m higher_lower
```
Type `A` or `B` and press Enter. Press Ctrl+C to quit.
## Run the tests
```bash
python -m pytest
```
Check the code style and types:
```bash
python -m ruff check src tests
python -m ruff format --check src tests
python -m mypy src tests
```
## Build the source documentation
The source uses Doxygen comments. The HTML output goes to `docs/doxygen/html`.
```bash
doxygen Doxyfile
```
## Project layout
| Path | Content |
| --- | --- |
| `src/higher_lower/` | The game: `game_logic.py` (pure functions), `game.py` (the loop), `game_data.py`, `art.py`, `constants.py` |
| `tests/` | pytest tests |
| `docs/` | Planning and review documents (business case, plan, milestones, use case) |
| `pyproject.toml` | Project configuration |
| `Doxyfile` | Doxygen configuration |
## License
GNU Affero General Public License v3.0. See [LICENSE](LICENSE).
+5 -1
View File
@@ -15,8 +15,12 @@ document of a type. `Primary File` may contain a glob (e.g.
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 | | MIL | Milestone / Gateway | docs/milestones/*.md | 005 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 005 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 013 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-*/uc.md | 002 |
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
## Languages ## Languages
+5 -2
View File
@@ -4,12 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | MIL-002 | | ID | MIL-002 |
| CrossReference | [BC-001], [PP-001] | | CrossReference | [BC-001], [PP-001], [RC-005] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [ddfe96f] | | 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [ddfe96f] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Accepted after review RC-005 | [ee58fcd] |
--- ---
@@ -72,3 +73,5 @@ The assignment data set and art as modules, constants in `constants.py`, and the
[PP-001]: ../project-plan.md [PP-001]: ../project-plan.md
[MIL-001]: ./mil-001-project-setup.md [MIL-001]: ./mil-001-project-setup.md
[ddfe96f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ddfe96ffa459d5e4b2e30d71bd53878aba2cfc5d [ddfe96f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ddfe96ffa459d5e4b2e30d71bd53878aba2cfc5d
[RC-005]: ../sqa/reviews/rc-005-mil-002.md
[ee58fcd]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ee58fcdc1c9c83b0721022d623e4989591a47987
+9 -3
View File
@@ -4,12 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | MIL-003 | | ID | MIL-003 |
| CrossReference | [BC-001], [PP-001] | | CrossReference | [BC-001], [PP-001], [US-001], [RC-006] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [ddfe96f] | | 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [ddfe96f] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Linked UC-001 and the user stories<br>Accepted after review RC-006 | [923dde7] |
--- ---
@@ -43,6 +44,7 @@ An interactive console game started with `python -m higher_lower`, built on the
| Business Case objective / KPI / user story | Reference | | Business Case objective / KPI / user story | Reference |
| --- | --- | | --- | --- |
| O1, O3 | [BC-001] | | O1, O3 | [BC-001] |
| US-001.01, US-001.02 | [US-001] |
## Ownership ## Ownership
@@ -60,7 +62,7 @@ An interactive console game started with `python -m higher_lower`, built on the
| # | Task | Summary | Needs its own Use Case/User Story? | Reference | | # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| 1 | Implement display and input helpers | A function that prints the logo, "Compare A: ...", the `vs` art and "Against B: ..." using `format_data`, and one that asks for "A" or "B" and re-asks until valid (case-insensitive). Input and output go through injectable callables so tests need no real console. | No | | | 1 | Implement display and input helpers | A function that prints the logo, "Compare A: ...", the `vs` art and "Against B: ..." using `format_data`, and one that asks for "A" or "B" and re-asks until valid (case-insensitive). Input and output go through injectable callables so tests need no real console. | No | |
| 2 | Implement the game loop | `play_game()` keeps the score, starts with a random pair, compares guesses with `check_answer`, moves B to A after a correct guess and picks a new B that differs from A. Prints the score after each round and "Sorry, that's wrong. Final score" at the end. | Yes | UC-001 Play a game (to be created with the SSD before this task starts) | | 2 | Implement the game loop | `play_game()` keeps the score, starts with a random pair, compares guesses with `check_answer`, moves B to A after a correct guess and picks a new B that differs from A. Prints the score after each round and "Sorry, that's wrong. Final score" at the end. | Yes | [UC-001], [US-001] |
| 3 | Handle exhausted data | If every account has been used, end the game with a win message instead of failing to find a new B. | No | | | 3 | Handle exhausted data | If every account has been used, end the game with a win message instead of failing to find a new B. | No | |
| 4 | Add entry point | `python -m higher_lower` runs `main()`, which calls `play_game()`; no side effects on import. | No | | | 4 | Add entry point | `python -m higher_lower` runs `main()`, which calls `play_game()`; no side effects on import. | No | |
| 5 | Write game loop tests | pytest tests with scripted input and a seeded random generator: correct streak, wrong first guess, invalid input then valid, data exhausted, and the score printed. | No | | | 5 | Write game loop tests | pytest tests with scripted input and a seeded random generator: correct streak, wrong first guess, invalid input then valid, data exhausted, and the score printed. | No | |
@@ -71,3 +73,7 @@ An interactive console game started with `python -m higher_lower`, built on the
[PP-001]: ../project-plan.md [PP-001]: ../project-plan.md
[MIL-002]: ./mil-002-game-logic.md [MIL-002]: ./mil-002-game-logic.md
[ddfe96f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ddfe96ffa459d5e4b2e30d71bd53878aba2cfc5d [ddfe96f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ddfe96ffa459d5e4b2e30d71bd53878aba2cfc5d
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[RC-006]: ../sqa/reviews/rc-006-mil-003.md
[923dde7]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/923dde7dc9b59d2c77eddc88336451193147c557
+5 -2
View File
@@ -4,12 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | MIL-004 | | ID | MIL-004 |
| CrossReference | [BC-001], [PP-001] | | CrossReference | [BC-001], [PP-001], [RC-011] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [ddfe96f] | | 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [ddfe96f] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Accepted after review RC-011 | [267e34f] |
--- ---
@@ -70,3 +71,5 @@ Decide whether the repository is ready to be shared with S02 and S03: documented
[PP-001]: ../project-plan.md [PP-001]: ../project-plan.md
[MIL-003]: ./mil-003-console-game.md [MIL-003]: ./mil-003-console-game.md
[ddfe96f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ddfe96ffa459d5e4b2e30d71bd53878aba2cfc5d [ddfe96f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ddfe96ffa459d5e4b2e30d71bd53878aba2cfc5d
[RC-011]: ../sqa/reviews/rc-011-mil-004.md
[267e34f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/267e34fc9f1fb9d76cc20032f3b09dc8e9dd82c7
+46
View File
@@ -0,0 +1,46 @@
# SQA Review Record: MIL-002 Game logic
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-005 |
| CrossReference | [MIL-002], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [ee58fcd] |
---
## Artifact Under Review
- Instance reviewed: [MIL-002]
- Checklist used: [QC-MIL-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | Data and art modules, constants, four named functions, Doxygen comments, pytest tests. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Six objectively checkable criteria. |
| 3 | Dependencies on other milestones are explicitly mapped (Optional) | Pass | Depends on MIL-001, with the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | O1, O2, O3. |
| 5 | Milestone owner and approving reviewer are identified | Pass | S01 and S01. |
| 6 | Milestone has a target date consistent with project constraints | Pass | 2026-10-09, inside the 2026-10-14 constraint in BC-001. |
## Overall Verdict
Go — all criteria pass. Code under `src/` and `tests/` for MIL-002 may start once the MIL-001 deliverables exist (its dependency). MIL-003 and MIL-004 stay Proposed. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Deliver MIL-001 (issues #1-#4) before starting MIL-002 tasks | S01 | 2026-10-06 |
---
[MIL-002]: ../../milestones/mil-002-game-logic.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[ee58fcd]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/ee58fcdc1c9c83b0721022d623e4989591a47987
+47
View File
@@ -0,0 +1,47 @@
# SQA Review Record: MIL-003 Console game
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-006 |
| CrossReference | [MIL-003], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [923dde7] |
---
## Artifact Under Review
- Instance reviewed: [MIL-003]
- Checklist used: [QC-MIL-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | A playable console game started with python -m higher_lower, with loop tests. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Six objectively checkable criteria. |
| 3 | Dependencies on other milestones are explicitly mapped (Optional) | Pass | Depends on MIL-002, with the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | O1, O3; stories US-001.01 and US-001.02. |
| 5 | Milestone owner and approving reviewer are identified | Pass | S01 and S01. |
| 6 | Milestone has a target date consistent with project constraints | Pass | 2026-10-12, inside the 2026-10-14 constraint in BC-001. |
## Overall Verdict
Go — all criteria pass. UC-001 and its SSD were created for task 2 (the game loop); UCD-001, US-001, UC-001 and SSD-001 stay Proposed until they are reviewed. MIL-004 stays Proposed. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Review UCD-001, US-001, UC-001 and SSD-001 against their QC checklists | S01 | 2026-10-12 |
| Add UCD-001 and US-001 to the CrossReference of BC-001 and PP-001 when those documents are next edited | S01 | 2026-10-14 |
---
[MIL-003]: ../../milestones/mil-003-console-game.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[923dde7]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/923dde7dc9b59d2c77eddc88336451193147c557
+48
View File
@@ -0,0 +1,48 @@
# SQA Review Record: UCD-001 Use Case Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-007 |
| CrossReference | [UCD-001], [QC-UCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [c7164fe] |
---
## Artifact Under Review
- Instance reviewed: [UCD-001]
- Checklist used: [QC-UCD-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Actors are defined with correct UML stereotypes (e.g. `<<System>>`, `<<Actor>>`) | Pass | Actor <<Actor>> and boundary <<System>> stereotyped. |
| 2 | System boundary is clearly drawn and labeled | Pass | Boundary rectangle labelled Higher Lower. |
| 3 | Include/extend relationships are used correctly per UML 2.5.1, not as generic "uses" arrows | Pass | No include/extend used; the Relationships section says so. |
| 4 | Every actor participates in at least one use case (no orphan actors) | Pass | Player is in Play a Game. |
| 5 | Diagram is traceable to a documented stakeholder need | Pass | Player traces to S01 and S02 in SA-001. |
| 6 | Use case names are verb phrases describing actor goals, not internal system operations | Pass | Play a Game is an actor goal. |
| 7 | Diagram is free of implementation detail (e.g. UI widgets, database tables) (Optional) | Pass | No UI or storage detail. |
| 8 | Actor and use case naming is consistent with corresponding Use Case and User Story documents (Optional) | Pass | Names match US-001 and UC-001. |
## Overall Verdict
Go — all applicable criteria pass. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | — | — |
---
[UCD-001]: ../../use-case-diagram.md
[QC-UCD-001]: ../../../framework/qc/qc-use-case-diagram.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+47
View File
@@ -0,0 +1,47 @@
# SQA Review Record: US-001 User Stories
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-008 |
| CrossReference | [US-001], [QC-US-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [c7164fe] |
---
## Artifact Under Review
- Instance reviewed: [US-001]
- Checklist used: [QC-US-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | INVEST check present, no exceptions. |
| 2 | Written in "As a / I want / So that" form | Pass | Both stories use As a / I want / so that. |
| 3 | Clear, testable acceptance criteria are included | Pass | Given/When/Then criteria on both stories. |
| 4 | Traceable to a use case or epic | Pass | Both trace to UC-001 and MIL-003. |
| 5 | Story is sized to fit within a single iteration (Optional) | Pass | Each fits one iteration. |
| 6 | Story statement avoids technical implementation detail (Optional) | Pass | Stated as player goals. |
| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | Role Player matches UCD-001. |
## Overall Verdict
Go — all applicable criteria pass. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | — | — |
---
[US-001]: ../../user-stories.md
[QC-US-001]: ../../../framework/qc/qc-user-story.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+52
View File
@@ -0,0 +1,52 @@
# SQA Review Record: UC-001 Use Case
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-009 |
| CrossReference | [UC-001], [QC-UC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [c7164fe] |
---
## Artifact Under Review
- Instance reviewed: [UC-001]
- Checklist used: [QC-UC-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | N-A | Applies to the Brief format only; this use case is Fully Dressed. |
| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | Applies to the Casual format only; this use case is Fully Dressed. |
| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Scope, level, actor, stakeholders, pre/postconditions, scenario, extensions, rules and open issues present. |
| 4 | Preconditions and postconditions are explicitly defined | Pass | Both defined. |
| 5 | Primary actor is explicitly stated | Pass | Player. |
| 6 | Stakeholders and their interests are stated | Pass | S01 and S02 with interests. |
| 7 | Main success scenario is written as clear, numbered steps | Pass | Seven numbered steps. |
| 8 | Alternative/exception flows correctly reference `<<include>>`/`<<extend>>` use cases where relevant, per UML 2.5.1 | Pass | No include/extend applies; stated. |
| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text (Optional) | Pass | Rules listed for steps 2, 3b and 5. |
| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Title and actor match UCD-001 and US-001. |
| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated (Optional) | Pass | Level: user goal. |
| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Goal wording, no UI or implementation detail. |
## Overall Verdict
Go — all applicable criteria pass. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | — | — |
---
[UC-001]: ../../uc-001/uc.md
[QC-UC-001]: ../../../framework/qc/qc-use-case.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+47
View File
@@ -0,0 +1,47 @@
# SQA Review Record: SSD-001 System Sequence Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-010 |
| CrossReference | [SSD-001], [QC-SSD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [c7164fe] |
---
## Artifact Under Review
- Instance reviewed: [SSD-001]
- Checklist used: [QC-SSD-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Input/output messages match the corresponding Use Case's main success scenario step-for-step | Pass | startGame and makeGuess cover steps 1 to 7 of UC-001. |
| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | Only Player and :System. |
| 3 | Object creation/destruction of the System instance handled explicitly where relevant (Optional) | Pass | Lifecycle notes state when the game starts and ends. |
| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed returns on every operation. |
| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | Alternative flows noted as out of scope. |
| 6 | Message names are verb phrases consistent with the use case's system responsibilities (Optional) | Pass | startGame, makeGuess. |
| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | Cites UC-001 Play a Game. |
## Overall Verdict
Go — all applicable criteria pass. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | — | — |
---
[SSD-001]: ../../uc-001/ssd.md
[QC-SSD-001]: ../../../framework/qc/qc-ssd.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+46
View File
@@ -0,0 +1,46 @@
# SQA Review Record: MIL-004 Documentation and release
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-011 |
| CrossReference | [MIL-004], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [267e34f] |
---
## Artifact Under Review
- Instance reviewed: [MIL-004]
- Checklist used: [QC-MIL-001]
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | README, Doxygen build, review records and a verified clean copy. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Six objectively checkable criteria. |
| 3 | Dependencies on other milestones are explicitly mapped (Optional) | Pass | Depends on MIL-003, with the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | O2, O4, O5. |
| 5 | Milestone owner and approving reviewer are identified | Pass | S01 and S01. |
| 6 | Milestone has a target date consistent with project constraints | Pass | 2026-10-14, the date in the BC-001 constraint. |
## Overall Verdict
Go — all criteria pass. Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | — | — |
---
[MIL-004]: ../../milestones/mil-004-docs-release.md
[QC-MIL-001]: ../../../framework/qc/qc-milestones-gateways.md
[267e34f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/267e34fc9f1fb9d76cc20032f3b09dc8e9dd82c7
+52
View File
@@ -0,0 +1,52 @@
# SQA Review Record: Source code of higher_lower
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-012 |
| CrossReference | [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [267e34f] |
---
## Artifact Under Review
- Instance reviewed: the Python source in `src/higher_lower/` and `tests/` at the commit of this review (source code has no artifact ID)
- Checklist used: [QC-PY-001]
## 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 and functions snake_case, constants UPPER_SNAKE, TypedDict PascalCase; ruff (E, F, I, UP, FLY, B) passes. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names say purpose (check_answer, pick_pair); i/n-style names are not used. |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass; the only suppression is E501 for the verbatim data file, with a comment in pyproject.toml. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | All signatures annotated; `mypy --strict src tests` reports no issues. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No bare except; `main` catches EOFError and KeyboardInterrupt on purpose and leaves with a goodbye. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults; the data list default is a read-only module constant. |
| 7 | Files, locks and connections are managed with context managers | N-A | The code opens no files, locks or connections. |
| 8 | Public modules, classes and functions have docstrings that say what, not how (Optional) | Pass | Every module, class and function has a Doxygen docstring; checked with an AST script. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | N-A | The game prints its interface to the console; there is no logging, and no secrets or personal data are printed. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No Design Class Diagram exists or is planned; the only class is the Account TypedDict, and the functions implement the operations of SSD-001. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 36 pytest tests named for behaviour, with scripted input and seeded randomness; no network, no order dependence. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment (Optional) | Pass | mypy strict runs without errors; no Any is used. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused (Optional) | Pass | No runtime dependencies; dev tools declared in pyproject.toml with version ranges. |
## Overall Verdict
Go — all applicable criteria pass. The review found a lint ordering error, format differences and strict-typing errors; they were fixed before this verdict (ruff import order and format, `Final` on the account key constants). Reviewer and author are both S01: the project has a single participant, so the framework's author/reviewer separation cannot be met. S01 accepted this deviation when requesting acceptance (2026-10-04).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | — | — |
---
[QC-PY-001]: ../../../framework/qc/qc-programming-python.md
[267e34f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/267e34fc9f1fb9d76cc20032f3b09dc8e9dd82c7
+25 -6
View File
@@ -9,7 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [a1ff735] | | 2026-10-04 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-007 to RC-010 for UCD-001, US-001, UC-001 and SSD-001 | [c7164fe] |
| 2026-10-04 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-011 and RC-012 | [267e34f] |
--- ---
@@ -26,13 +27,18 @@ target is measurable. A row is added or updated whenever an artifact instance is
| [SA-001] | SA | [BC-001] | [PP-001] | [RC-002] | | [SA-001] | SA | [BC-001] | [PP-001] | [RC-002] |
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004] | [RC-003] | | [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004] | [RC-003] |
| [MIL-001] | MIL | [BC-001], [PP-001] | - | [RC-004] | | [MIL-001] | MIL | [BC-001], [PP-001] | - | [RC-004] |
| [MIL-002] | MIL | [BC-001], [PP-001] | - | - | | [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | - | [RC-005] |
| [MIL-003] | MIL | [BC-001], [PP-001] | - | - | | [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002], [US-001] | [MIL-004], [UC-001] | [RC-006] |
| [MIL-004] | MIL | [BC-001], [PP-001] | - | - | | [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-011] |
| [UCD-001] | UCD | [SA-001], [BC-001] | [US-001], [UC-001] | [RC-007] |
| [US-001] | US | [UCD-001], [BC-001], [MIL-003] | [UC-001] | [RC-008] |
| [UC-001] | UC | [UCD-001], [US-001], [SA-001] | [SSD-001] | [RC-009] |
| [SSD-001] | SSD | [UC-001] | - | [RC-010] |
## Coverage Notes ## Coverage Notes
- No use cases, user stories or design artifacts exist yet (UC-001 is planned for MIL-003). - No domain model or design artifacts exist yet.
- RC-012 reviews the Python source against QC-PY-001; source code has no artifact ID, so it has no row above.
- `-` in Upstream means foundational; in Downstream, nothing is built on it yet; in Last Reviewed, no `RC-*` exists yet. - `-` in Upstream means foundational; in Downstream, nothing is built on it yet; in Last Reviewed, no `RC-*` exists yet.
--- ---
@@ -48,4 +54,17 @@ target is measurable. A row is added or updated whenever an artifact instance is
[RC-002]: ./reviews/rc-002-stakeholder-analysis.md [RC-002]: ./reviews/rc-002-stakeholder-analysis.md
[RC-003]: ./reviews/rc-003-project-plan.md [RC-003]: ./reviews/rc-003-project-plan.md
[RC-004]: ./reviews/rc-004-mil-001.md [RC-004]: ./reviews/rc-004-mil-001.md
[a1ff735]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/a1ff73580fdcfad5bbb3854298fdfa0c16ada853 [RC-005]: ./reviews/rc-005-mil-002.md
[UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[SSD-001]: ../uc-001/ssd.md
[RC-006]: ./reviews/rc-006-mil-003.md
[RC-007]: ./reviews/rc-007-ucd-001.md
[RC-008]: ./reviews/rc-008-us-001.md
[RC-009]: ./reviews/rc-009-uc-001.md
[RC-010]: ./reviews/rc-010-ssd-001.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
[RC-011]: ./reviews/rc-011-mil-004.md
[RC-012]: ./reviews/rc-012-source-code.md
[267e34f]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/267e34fc9f1fb9d76cc20032f3b09dc8e9dd82c7
+57
View File
@@ -0,0 +1,57 @@
# System Sequence Diagram: Play a Game
## Metadata
| Key | Value |
| --- | --- |
| ID | SSD-001 |
| CrossReference | [UC-001], [RC-010] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [923dde7] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Accepted after review RC-010 | [c7164fe] |
---
## Source Use Case
[UC-001] Play a Game, main success scenario. The alternative flows (invalid answer, tie, no account left) are out of scope for this diagram.
## Diagram
```plantuml
@startuml
actor Player
participant ":System" as System
Player -> System : startGame()
System --> Player : accounts A and B
loop while the guess is right
Player -> System : makeGuess(choice)
System --> Player : right, score, new accounts A and B
end
Player -> System : makeGuess(choice)
System --> Player : wrong, final score
@enduml
```
## System Operations Table
| Step | Message (verb phrase) | Parameters | Return | Use case step |
| --- | --- | --- | --- | --- |
| 1 | startGame | none | accounts A and B | 1, 2 |
| 2 | makeGuess | choice (A or B) | right, new score, new accounts A and B | 3, 4, 5, 6 |
| 3 | makeGuess | choice (A or B) | wrong, final score | 3, 7 |
Steps 2 and 3 are the same operation; the loop shows the right-guess outcome repeating, and the last call shows the wrong-guess outcome.
## Lifecycle Notes
A game starts with `startGame` and ends when `makeGuess` returns wrong; the System keeps the score and the current accounts in between.
---
[UC-001]: ./uc.md
[923dde7]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/923dde7dc9b59d2c77eddc88336451193147c557
[RC-010]: ../sqa/reviews/rc-010-ssd-001.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+81
View File
@@ -0,0 +1,81 @@
# UC-001 Play a Game
## Metadata
| Key | Value |
| --- | --- |
| ID | UC-001 |
| CrossReference | [UCD-001], [US-001], [SA-001], [SSD-001], [RC-009] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [923dde7] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Accepted after review RC-009 | [c7164fe] |
---
Format: Fully Dressed
## Scope
The Higher Lower console game.
## Level
User goal.
## Primary Actor
Player.
## Stakeholders and Interests
| Stakeholder | Interest |
| --- | --- |
| S01 | A correct game that shows good practice ([SA-001]) |
| S02 | Readable behaviour that matches the assignment ([SA-001]) |
## Preconditions
The Player has started the game.
## Postconditions (Success Guarantee)
The Player has seen the final score of the game.
## Main Success Scenario
1. The Player starts a game.
2. The System shows two different accounts, A and B.
3. The Player guesses which account has more followers.
4. The System tells the Player the guess was right, adds one to the score, and shows the new score.
5. The System makes the account the Player compared against the new A and shows a new B.
6. The Player repeats from step 3 until a guess is wrong.
7. The System tells the Player the guess was wrong and shows the final score.
## Extensions / Alternative Flows
- 3a. The Player gives an answer that is not a valid choice: the System asks again; the round does not change.
- 3b. Both accounts have the same number of followers: either choice counts as right.
- 5a. No account is left that differs from the current A: the System ends the game and shows the final score.
No `<<include>>` or `<<extend>>` use cases apply.
## Special Requirements / Business Rules
- Step 2 and 5: A and B are never the same account.
- Step 3b: equal follower counts make both choices right.
## Open Issues
None.
---
[UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md
[SA-001]: ../stakeholder-analysis.md
[SSD-001]: ./ssd.md
[923dde7]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/923dde7dc9b59d2c77eddc88336451193147c557
[RC-009]: ../sqa/reviews/rc-009-uc-001.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+58
View File
@@ -0,0 +1,58 @@
# Use Case Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | UCD-001 |
| CrossReference | [SA-001], [BC-001], [US-001], [UC-001], [RC-007] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [923dde7] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Accepted after review RC-007 | [c7164fe] |
---
## Purpose and Scope
The system boundary is the Higher Lower console game. One actor, the Player, uses it to play a game. Traces to [BC-001] objective O1 and to stakeholders S01 and S02 ([SA-001]), who play the game; S03 only reads the repository.
## Diagram
```plantuml
@startuml
left to right direction
actor Player <<Actor>>
rectangle "Higher Lower <<System>>" {
usecase "Play a Game" as PlayAGame
}
Player --> PlayAGame
@enduml
```
## Actor Table
| Actor | Stereotype | Stakeholder ID (SA) | Goals (use cases) |
| --- | --- | --- | --- |
| Player | Actor | S01, S02 | Play a Game |
## Use Case Table
| Use Case | Actor(s) | Goal |
| --- | --- | --- |
| Play a Game ([UC-001]) | Player | Guess, round after round, which of two accounts has more followers, and see the final score |
## Relationships
None: there is a single use case, so no `<<include>>` or `<<extend>>` is used.
---
[SA-001]: ./stakeholder-analysis.md
[BC-001]: ./business-case.md
[US-001]: ./user-stories.md
[UC-001]: ./uc-001/uc.md
[923dde7]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/923dde7dc9b59d2c77eddc88336451193147c557
[RC-007]: ./sqa/reviews/rc-007-ucd-001.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+54
View File
@@ -0,0 +1,54 @@
# User Stories
## Metadata
| Key | Value |
| --- | --- |
| ID | US-001 |
| CrossReference | [UCD-001], [UC-001], [BC-001], [MIL-003], [RC-008] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-04 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version | [923dde7] |
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Accepted after review RC-008 | [c7164fe] |
---
## Purpose and Scope
The stories for epic [MIL-003] (Console game): playing rounds and seeing the result. They support [BC-001] objective O1.
## Story List
### US-001.01 Guess which account has more followers
- **As a** Player, **I want** to guess which of two accounts has more followers and keep playing while I am right, **so that** I can build a streak.
- **Acceptance Criteria:**
- Given a new game, when it starts, then two different accounts A and B are shown.
- Given two accounts, when I guess the one with more followers, then my score goes up by one and the next round starts with that account as A and a new account as B.
- Given an invalid answer, when I submit it, then I am asked again and the round does not change.
- **Traces to:** [UC-001], [MIL-003]
- **Size:** one iteration.
### US-001.02 See my final score
- **As a** Player, **I want** to see my final score when the game ends, **so that** I know how well I did.
- **Acceptance Criteria:**
- Given a round, when I guess the account with fewer followers, then the game ends and shows my final score.
- Given every account has been used, when no new account can be shown, then the game ends and shows my final score.
- **Traces to:** [UC-001], [MIL-003]
- **Size:** one iteration.
## INVEST Check
Both stories are Independent of each other in wording, Negotiable in detail, Valuable to the Player, Estimable, Small (one iteration) and Testable through the acceptance criteria. No exceptions.
---
[UCD-001]: ./use-case-diagram.md
[UC-001]: ./uc-001/uc.md
[BC-001]: ./business-case.md
[MIL-003]: ./milestones/mil-003-console-game.md
[923dde7]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/923dde7dc9b59d2c77eddc88336451193147c557
[RC-008]: ./sqa/reviews/rc-008-us-001.md
[c7164fe]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/014-higher_lower/commit/c7164feab1333951bf6f557baf95a26764656be0
+35
View File
@@ -0,0 +1,35 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "higher-lower"
version = "0.1.0"
description = "Higher Lower console game: guess which Instagram account has more followers (Udemy 100 Days of Code, day 14)."
readme = "README.md"
requires-python = ">=3.13"
license = { file = "LICENSE" }
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=9,<10", "ruff>=0.16,<0.17", "mypy>=2.4,<3"]
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "FLY", "B"]
[tool.ruff.lint.per-file-ignores]
"src/higher_lower/game_data.py" = ["E501"] # assignment data kept verbatim
[tool.mypy]
strict = true
mypy_path = "src"
+3
View File
@@ -0,0 +1,3 @@
"""@package higher_lower
@brief Higher Lower console game: guess which Instagram account has more followers.
"""
+19
View File
@@ -0,0 +1,19 @@
"""@file __main__.py
@brief Entry point for `python -m higher_lower`.
"""
from higher_lower.constants import MSG_GOODBYE
from higher_lower.game import play_game
def main() -> None:
"""@brief Play a game; leave quietly if the player quits with Ctrl+C or Ctrl+D."""
try:
play_game()
except (EOFError, KeyboardInterrupt):
print()
print(MSG_GOODBYE)
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
"""@file art.py
@brief ASCII art shown by the game, as given by the assignment.
The lines are raw strings so the backslashes in the art survive unchanged.
They are concatenated line by line, rather than written as one multi-line
string, so Doxygen does not read the backticks in the art as markup.
"""
## The game logo.
logo = (
"\n"
r" __ ___ __ "
"\n"
r" / / / (_)___ _/ /_ ___ _____"
"\n"
r" / /_/ / / __ `/ __ \/ _ \/ ___/"
"\n"
r" / __ / / /_/ / / / / __/ / "
"\n"
r"/_/ ///_/\__, /_/ /_/\___/_/ "
"\n"
r" / / /____/_ _____ _____"
"\n"
r" / / / __ \ | /| / / _ \/ ___/"
"\n"
r" / /___/ /_/ / |/ |/ / __/ / "
"\n"
r"/_____/\____/|__/|__/\___/_/ "
"\n"
)
## The "vs" shown between the two accounts.
vs = (
"\n"
r" _ __ "
"\n"
r"| | / /____"
"\n"
r"| | / / ___/"
"\n"
r"| |/ (__ ) "
"\n"
r"|___/____(_)"
"\n"
)
+31
View File
@@ -0,0 +1,31 @@
"""@file constants.py
@brief Constants shared by the game modules.
"""
from typing import Final
## Keys of an account dictionary.
KEY_NAME: Final = "name"
KEY_FOLLOWER_COUNT: Final = "follower_count"
KEY_DESCRIPTION: Final = "description"
KEY_COUNTRY: Final = "country"
## The two choices the player can type, in lower case.
CHOICE_A = "a"
CHOICE_B = "b"
## Score at the start of a game.
START_SCORE = 0
## How an account is described to the player.
ACCOUNT_TEMPLATE = "{name}, a {description}, from {country}"
## Texts shown to the player.
MSG_COMPARE_A = "Compare A: {account}."
MSG_AGAINST_B = "Against B: {account}."
PROMPT_GUESS = "Who has more followers? Type 'A' or 'B': "
MSG_INVALID_CHOICE = "Please type 'A' or 'B'."
MSG_CORRECT = "You're right! Current score: {score}."
MSG_WRONG = "Sorry, that's wrong. Final score: {score}"
MSG_WIN = "You went through every account! Final score: {score}"
MSG_GOODBYE = "Goodbye!"
+122
View File
@@ -0,0 +1,122 @@
"""@file game.py
@brief The interactive game: what the player sees, what they type, and the game loop.
Input and output go through callables so tests can script a whole game.
"""
import random
from collections.abc import Callable, Sequence
from higher_lower.art import logo, vs
from higher_lower.constants import (
CHOICE_A,
CHOICE_B,
KEY_FOLLOWER_COUNT,
KEY_NAME,
MSG_AGAINST_B,
MSG_COMPARE_A,
MSG_CORRECT,
MSG_INVALID_CHOICE,
MSG_WIN,
MSG_WRONG,
PROMPT_GUESS,
START_SCORE,
)
from higher_lower.game_data import Account, data
from higher_lower.game_logic import (
check_answer,
format_data,
get_random_account,
pick_pair,
)
InputFunc = Callable[[str], str]
OutputFunc = Callable[[str], None]
def show_round(
account_a: Account,
account_b: Account,
output: OutputFunc = print,
) -> None:
"""@brief Show the two accounts the player compares.
@param account_a The account shown as A.
@param account_b The account shown as B.
@param output Where the text goes.
"""
output(MSG_COMPARE_A.format(account=format_data(account_a)))
output(vs)
output(MSG_AGAINST_B.format(account=format_data(account_b)))
def ask_choice(input_func: InputFunc = input, output: OutputFunc = print) -> str:
"""@brief Ask for A or B until the player gives a valid answer.
@param input_func Reads one line of input, given the prompt.
@param output Where the invalid-answer message goes.
@return "a" or "b", in lower case.
"""
while True:
answer = input_func(PROMPT_GUESS).strip().lower()
if answer in (CHOICE_A, CHOICE_B):
return answer
output(MSG_INVALID_CHOICE)
def _pick_next_account(
accounts: Sequence[Account],
shown_names: set[str],
rng: random.Random | None,
) -> Account | None:
"""@brief Draw an account that has not been shown in this game.
@param accounts All accounts of the game.
@param shown_names Names of the accounts already shown.
@param rng Random generator, or None for the default.
@return A new account, or None if every account has been shown.
"""
unused = [account for account in accounts if account[KEY_NAME] not in shown_names]
if not unused:
return None
return get_random_account(unused, rng)
def play_game(
accounts: Sequence[Account] = data,
rng: random.Random | None = None,
input_func: InputFunc = input,
output: OutputFunc = print,
) -> int:
"""@brief Play one game, from the logo to the final score.
Each account is shown once per game. A right guess adds a point and
makes B the next A; a wrong guess, or running out of accounts, ends
the game.
@param accounts The accounts to play with; needs at least two entries.
@param rng Random generator; pass a seeded one for a repeatable game.
@param input_func Reads one line of input, given the prompt.
@param output Where the text goes.
@return The final score.
"""
output(logo)
score = START_SCORE
account_a, account_b = pick_pair(accounts, rng)
shown_names = {account_a[KEY_NAME], account_b[KEY_NAME]}
while True:
show_round(account_a, account_b, output)
guess = ask_choice(input_func, output)
if not check_answer(
guess, account_a[KEY_FOLLOWER_COUNT], account_b[KEY_FOLLOWER_COUNT]
):
output(MSG_WRONG.format(score=score))
return score
score += 1
output(MSG_CORRECT.format(score=score))
next_account = _pick_next_account(accounts, shown_names, rng)
if next_account is None:
output(MSG_WIN.format(score=score))
return score
account_a, account_b = account_b, next_account
shown_names.add(next_account[KEY_NAME])
+324
View File
@@ -0,0 +1,324 @@
"""@file game_data.py
@brief The accounts the player compares, as given by the assignment.
The order is kept as in the assignment; the game compares follower counts,
never list positions.
"""
from typing import TypedDict
class Account(TypedDict):
"""@brief One account the player can be asked about.
follower_count is in millions of Instagram followers.
"""
name: str
follower_count: int
description: str
country: str
data: list[Account] = [
{
"name": "Instagram",
"follower_count": 346,
"description": "Social media platform",
"country": "United States",
},
{
"name": "Cristiano Ronaldo",
"follower_count": 215,
"description": "Footballer",
"country": "Portugal",
},
{
"name": "Ariana Grande",
"follower_count": 183,
"description": "Musician and actress",
"country": "United States",
},
{
"name": "Dwayne Johnson",
"follower_count": 181,
"description": "Actor and professional wrestler",
"country": "United States",
},
{
"name": "Selena Gomez",
"follower_count": 174,
"description": "Musician and actress",
"country": "United States",
},
{
"name": "Kylie Jenner",
"follower_count": 172,
"description": "Reality TV personality and businesswoman and Self-Made Billionaire",
"country": "United States",
},
{
"name": "Kim Kardashian",
"follower_count": 167,
"description": "Reality TV personality and businesswoman",
"country": "United States",
},
{
"name": "Lionel Messi",
"follower_count": 149,
"description": "Footballer",
"country": "Argentina",
},
{
"name": "Beyoncé",
"follower_count": 145,
"description": "Musician",
"country": "United States",
},
{
"name": "Neymar",
"follower_count": 138,
"description": "Footballer",
"country": "Brasil",
},
{
"name": "National Geographic",
"follower_count": 135,
"description": "Magazine",
"country": "United States",
},
{
"name": "Justin Bieber",
"follower_count": 133,
"description": "Musician",
"country": "Canada",
},
{
"name": "Taylor Swift",
"follower_count": 131,
"description": "Musician",
"country": "United States",
},
{
"name": "Kendall Jenner",
"follower_count": 127,
"description": "Reality TV personality and Model",
"country": "United States",
},
{
"name": "Jennifer Lopez",
"follower_count": 119,
"description": "Musician and actress",
"country": "United States",
},
{
"name": "Nicki Minaj",
"follower_count": 113,
"description": "Musician",
"country": "Trinidad and Tobago",
},
{
"name": "Nike",
"follower_count": 109,
"description": "Sportswear multinational",
"country": "United States",
},
{
"name": "Khloé Kardashian",
"follower_count": 108,
"description": "Reality TV personality and businesswoman",
"country": "United States",
},
{
"name": "Miley Cyrus",
"follower_count": 107,
"description": "Musician and actress",
"country": "United States",
},
{
"name": "Katy Perry",
"follower_count": 94,
"description": "Musician",
"country": "United States",
},
{
"name": "Kourtney Kardashian",
"follower_count": 90,
"description": "Reality TV personality",
"country": "United States",
},
{
"name": "Kevin Hart",
"follower_count": 89,
"description": "Comedian and actor",
"country": "United States",
},
{
"name": "Ellen DeGeneres",
"follower_count": 87,
"description": "Comedian",
"country": "United States",
},
{
"name": "Real Madrid CF",
"follower_count": 86,
"description": "Football club",
"country": "Spain",
},
{
"name": "FC Barcelona",
"follower_count": 85,
"description": "Football club",
"country": "Spain",
},
{
"name": "Rihanna",
"follower_count": 81,
"description": "Musician and businesswoman",
"country": "Barbados",
},
{
"name": "Demi Lovato",
"follower_count": 80,
"description": "Musician and actress",
"country": "United States",
},
{
"name": "Victoria's Secret",
"follower_count": 69,
"description": "Lingerie brand",
"country": "United States",
},
{
"name": "Zendaya",
"follower_count": 68,
"description": "Actress and musician",
"country": "United States",
},
{
"name": "Shakira",
"follower_count": 66,
"description": "Musician",
"country": "Colombia",
},
{
"name": "Drake",
"follower_count": 65,
"description": "Musician",
"country": "Canada",
},
{
"name": "Chris Brown",
"follower_count": 64,
"description": "Musician",
"country": "United States",
},
{
"name": "LeBron James",
"follower_count": 63,
"description": "Basketball player",
"country": "United States",
},
{
"name": "Vin Diesel",
"follower_count": 62,
"description": "Actor",
"country": "United States",
},
{
"name": "Cardi B",
"follower_count": 67,
"description": "Musician",
"country": "United States",
},
{
"name": "David Beckham",
"follower_count": 82,
"description": "Footballer",
"country": "United Kingdom",
},
{
"name": "Billie Eilish",
"follower_count": 61,
"description": "Musician",
"country": "United States",
},
{
"name": "Justin Timberlake",
"follower_count": 59,
"description": "Musician and actor",
"country": "United States",
},
{
"name": "UEFA Champions League",
"follower_count": 58,
"description": "Club football competition",
"country": "Europe",
},
{
"name": "NASA",
"follower_count": 56,
"description": "Space agency",
"country": "United States",
},
{
"name": "Emma Watson",
"follower_count": 56,
"description": "Actress",
"country": "United Kingdom",
},
{
"name": "Shawn Mendes",
"follower_count": 57,
"description": "Musician",
"country": "Canada",
},
{
"name": "Virat Kohli",
"follower_count": 55,
"description": "Cricketer",
"country": "India",
},
{
"name": "Gigi Hadid",
"follower_count": 54,
"description": "Model",
"country": "United States",
},
{
"name": "Priyanka Chopra Jonas",
"follower_count": 53,
"description": "Actress and musician",
"country": "India",
},
{
"name": "9GAG",
"follower_count": 52,
"description": "Social media platform",
"country": "China",
},
{
"name": "Ronaldinho",
"follower_count": 51,
"description": "Footballer",
"country": "Brasil",
},
{
"name": "Maluma",
"follower_count": 50,
"description": "Musician",
"country": "Colombia",
},
{
"name": "Camila Cabello",
"follower_count": 49,
"description": "Musician",
"country": "Cuba",
},
{
"name": "NBA",
"follower_count": 47,
"description": "Club Basketball Competition",
"country": "United States",
},
]
+78
View File
@@ -0,0 +1,78 @@
"""@file game_logic.py
@brief Pure game functions: draw accounts, describe them, check guesses.
"""
import random
from collections.abc import Sequence
from higher_lower.constants import (
ACCOUNT_TEMPLATE,
CHOICE_A,
CHOICE_B,
KEY_COUNTRY,
KEY_DESCRIPTION,
KEY_NAME,
)
from higher_lower.game_data import Account, data
_DEFAULT_RNG = random.Random()
def get_random_account(
accounts: Sequence[Account] = data,
rng: random.Random | None = None,
) -> Account:
"""@brief Return one random account.
@param accounts The accounts to draw from.
@param rng Random generator; pass a seeded one to make the draw repeatable.
@return A random entry of accounts.
"""
return (rng or _DEFAULT_RNG).choice(accounts)
def pick_pair(
accounts: Sequence[Account] = data,
rng: random.Random | None = None,
) -> tuple[Account, Account]:
"""@brief Return two different random accounts.
@param accounts The accounts to draw from; needs at least two entries.
@param rng Random generator; pass a seeded one to make the draw repeatable.
@return The accounts A and B, never the same entry.
@throws ValueError If accounts has fewer than two entries.
"""
first, second = (rng or _DEFAULT_RNG).sample(accounts, 2)
return first, second
def format_data(account: Account) -> str:
"""@brief Describe an account for the player.
@param account The account to describe.
@return Text such as "Nike, a Sportswear multinational, from United States".
"""
return ACCOUNT_TEMPLATE.format(
name=account[KEY_NAME],
description=account[KEY_DESCRIPTION],
country=account[KEY_COUNTRY],
)
def check_answer(guess: str, a_followers: int, b_followers: int) -> bool:
"""@brief Tell whether the guess names the account with more followers.
Equal follower counts make either choice correct.
@param guess "a" or "b", in any case.
@param a_followers Follower count of account A.
@param b_followers Follower count of account B.
@return True if the guess is right.
@throws ValueError If guess is neither "a" nor "b".
"""
choice = guess.lower()
if choice == CHOICE_A:
return a_followers >= b_followers
if choice == CHOICE_B:
return b_followers >= a_followers
raise ValueError(f"guess must be {CHOICE_A!r} or {CHOICE_B!r}, got {guess!r}")
+161
View File
@@ -0,0 +1,161 @@
"""@file test_game.py
@brief Tests for the game loop, run with scripted input and no real console.
"""
import io
import random
from collections.abc import Callable, Iterable, Sequence
import pytest
from higher_lower import __main__ as entry_point
from higher_lower.art import logo
from higher_lower.game import ask_choice, play_game, show_round
from higher_lower.game_data import Account
ACCOUNTS: list[Account] = [
{
"name": "First",
"follower_count": 30,
"description": "Test one",
"country": "Denmark",
},
{
"name": "Second",
"follower_count": 20,
"description": "Test two",
"country": "Norway",
},
{
"name": "Third",
"follower_count": 10,
"description": "Test three",
"country": "Sweden",
},
]
class FirstRng(random.Random):
"""@brief Random generator that takes the first entries, so a game is fixed."""
def sample( # type: ignore[override]
self, population: Sequence[Account], k: int, **kwargs: object
) -> list[Account]:
"""@brief Return the first k entries."""
return list(population)[:k]
def choice(self, seq: Sequence[Account]) -> Account: # type: ignore[override]
"""@brief Return the first entry."""
return seq[0]
def scripted(answers: Iterable[str]) -> Callable[[str], str]:
"""@brief Make an input function that gives the answers one by one.
@param answers The answers to give.
@return A function with the signature of input().
"""
iterator = iter(answers)
return lambda _prompt: next(iterator)
def run_game(answers: Iterable[str]) -> tuple[int, str]:
"""@brief Play a fixed game and return the score and everything shown.
@param answers The answers the player gives.
@return The final score and the output joined with newlines.
"""
lines: list[str] = []
score = play_game(ACCOUNTS, FirstRng(), scripted(answers), lines.append)
return score, "\n".join(lines)
def test_show_round_shows_both_accounts() -> None:
"""@brief Both accounts are described, with the vs art between them."""
lines: list[str] = []
show_round(ACCOUNTS[0], ACCOUNTS[1], lines.append)
assert lines[0] == "Compare A: First, a Test one, from Denmark."
assert lines[2] == "Against B: Second, a Test two, from Norway."
@pytest.mark.parametrize("typed", ["a", "A", " a "])
def test_ask_choice_accepts_a(typed: str) -> None:
"""@brief A is accepted in any case and with spaces around it."""
assert ask_choice(scripted([typed]), lambda _line: None) == "a"
def test_ask_choice_asks_again_after_an_invalid_answer() -> None:
"""@brief Invalid answers are rejected and the player is asked again."""
lines: list[str] = []
assert ask_choice(scripted(["x", "", "b"]), lines.append) == "b"
assert len(lines) == 2
def test_correct_streak_adds_a_point_per_round() -> None:
"""@brief Each right guess adds one to the score and B becomes the next A."""
score, shown = run_game(["a", "a"])
assert score == 2
assert "Current score: 1." in shown
assert "Compare A: Second" in shown
def test_wrong_first_guess_ends_the_game_with_zero() -> None:
"""@brief A wrong guess ends the game and shows the final score."""
score, shown = run_game(["b"])
assert score == 0
assert "Sorry, that's wrong. Final score: 0" in shown
def test_wrong_guess_after_a_point_keeps_the_score() -> None:
"""@brief The final score is the number of right guesses."""
score, shown = run_game(["a", "b"])
assert score == 1
assert "Final score: 1" in shown
def test_invalid_input_does_not_change_the_round() -> None:
"""@brief An invalid answer is re-asked without ending the game or scoring."""
score, shown = run_game(["x", "a", "a"])
assert score == 2
assert shown.count("Compare A: First") == 1
def test_game_ends_with_a_win_when_the_accounts_run_out() -> None:
"""@brief Using every account ends the game instead of failing."""
score, shown = run_game(["a", "a"])
assert score == 2
assert "You went through every account!" in shown
def test_game_starts_with_the_logo() -> None:
"""@brief The logo is shown first."""
_score, shown = run_game(["b"])
assert shown.startswith(logo)
def test_full_game_with_the_assignment_data_ends() -> None:
"""@brief A game with the real data set ends with a final score."""
lines: list[str] = []
score = play_game(
rng=random.Random(5), input_func=lambda _prompt: "a", output=lines.append
)
assert score >= 0
assert "Final score" in lines[-1]
def test_main_plays_a_game(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""@brief main() runs a game on the console."""
monkeypatch.setattr("sys.stdin", io.StringIO("a\n" * 100))
entry_point.main()
assert "Final score" in capsys.readouterr().out
def test_main_leaves_quietly_on_end_of_input(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""@brief Ctrl+D ends the game with a goodbye, not a traceback."""
monkeypatch.setattr("sys.stdin", io.StringIO(""))
entry_point.main()
assert "Goodbye!" in capsys.readouterr().out
+46
View File
@@ -0,0 +1,46 @@
"""@file test_game_data.py
@brief Tests for the assignment data set and art.
"""
from higher_lower.art import logo, vs
from higher_lower.constants import (
KEY_COUNTRY,
KEY_DESCRIPTION,
KEY_FOLLOWER_COUNT,
KEY_NAME,
)
from higher_lower.game_data import data
EXPECTED_KEYS = {KEY_NAME, KEY_FOLLOWER_COUNT, KEY_DESCRIPTION, KEY_COUNTRY}
def test_data_has_fifty_accounts() -> None:
"""@brief The assignment data set has 50 entries."""
assert len(data) == 50
def test_every_account_has_the_required_keys() -> None:
"""@brief Every entry has exactly the four assignment keys."""
assert all(set(account) == EXPECTED_KEYS for account in data)
def test_follower_counts_are_positive_integers() -> None:
"""@brief Follower counts are positive integers."""
assert all(
isinstance(account[KEY_FOLLOWER_COUNT], int) and account[KEY_FOLLOWER_COUNT] > 0
for account in data
)
def test_account_names_are_unique() -> None:
"""@brief No account appears twice."""
names = [account[KEY_NAME] for account in data]
assert len(names) == len(set(names))
def test_art_is_available() -> None:
"""@brief The logo and the vs art keep their shape, including backslashes."""
assert len(logo.splitlines()) == 10
assert "\\" in logo
assert len(vs.splitlines()) == 6
assert "|___/____(_)" in vs
+88
View File
@@ -0,0 +1,88 @@
"""@file test_game_logic.py
@brief Tests for the pure game functions.
"""
import random
import pytest
from higher_lower.constants import KEY_NAME
from higher_lower.game_data import Account, data
from higher_lower.game_logic import (
check_answer,
format_data,
get_random_account,
pick_pair,
)
def test_get_random_account_returns_an_entry_of_the_data() -> None:
"""@brief The drawn account comes from the data set."""
assert get_random_account(rng=random.Random(1)) in data
def test_get_random_account_is_repeatable_with_a_seed() -> None:
"""@brief The same seed draws the same account."""
assert get_random_account(rng=random.Random(7)) == get_random_account(
rng=random.Random(7)
)
def test_pick_pair_never_returns_the_same_account() -> None:
"""@brief A and B are always different entries."""
rng = random.Random(0)
for _ in range(200):
first, second = pick_pair(rng=rng)
assert first[KEY_NAME] != second[KEY_NAME]
def test_pick_pair_with_two_accounts_returns_both() -> None:
"""@brief With only two accounts, the pair holds both of them."""
two = data[:2]
first, second = pick_pair(two, random.Random(3))
assert {first[KEY_NAME], second[KEY_NAME]} == {a[KEY_NAME] for a in two}
def test_pick_pair_with_one_account_raises() -> None:
"""@brief A pair cannot be drawn from a single account."""
with pytest.raises(ValueError):
pick_pair(data[:1])
def test_format_data_describes_the_account() -> None:
"""@brief The description holds name, description and country."""
account: Account = {
"name": "Nike",
"follower_count": 109,
"description": "Sportswear multinational",
"country": "United States",
}
assert (
format_data(account) == "Nike, a Sportswear multinational, from United States"
)
@pytest.mark.parametrize(
("guess", "a_followers", "b_followers", "expected"),
[
("a", 200, 100, True),
("a", 100, 200, False),
("b", 100, 200, True),
("b", 200, 100, False),
("A", 200, 100, True),
("B", 100, 200, True),
("a", 100, 100, True),
("b", 100, 100, True),
],
)
def test_check_answer(
guess: str, a_followers: int, b_followers: int, expected: bool
) -> None:
"""@brief The answer is right for the higher account, and for either on a tie."""
assert check_answer(guess, a_followers, b_followers) is expected
def test_check_answer_rejects_an_unknown_choice() -> None:
"""@brief A guess other than a or b is an error."""
with pytest.raises(ValueError):
check_answer("c", 1, 2)
+16
View File
@@ -0,0 +1,16 @@
"""@file test_smoke.py
@brief Smoke test: the package and its entry point can be imported.
"""
import higher_lower
from higher_lower import __main__ as entry_point
def test_package_imports() -> None:
"""@brief The package imports without side effects."""
assert higher_lower.__doc__
def test_entry_point_is_callable() -> None:
"""@brief The entry point exists; importing it does not start a game."""
assert callable(entry_point.main)