Merge pull request 'MIL-002 Game core logic' (#26) from mil-002-game-core into main
This commit was merged in pull request #26.
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# Read the states with the csv module
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | ADR-0001 |
|
||||||
|
| CrossReference | [BC-001], [SA-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-13 | Accepted | Claude (AI) for S01 | AI as stand-in for S01 | Initial version; review RC-008: Go | [bb8730c] |
|
||||||
|
|
||||||
|
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The lectures read `50_states.csv` with pandas (`pandas.read_csv`, a DataFrame, then `DataFrame.to_csv` for `states_to_learn.csv`). The file has 50 rows and three columns (`state`, `x`, `y`). [BC-001] sets the constraint "avoid adding runtime dependencies unless they are needed", and [SA-001] records that S03 wants no runtime dependencies while S02 wants the assignment's function and variable names.
|
||||||
|
|
||||||
|
Options evaluated:
|
||||||
|
|
||||||
|
1. **pandas**: matches the lectures literally; adds a large runtime dependency (with NumPy) for 50 rows and makes installation and CI slower.
|
||||||
|
2. **Standard library `csv` module**: no dependency; a few more lines to read and write; the assignment's names (`data`, `states`, `guessed_states`, `missing_states`) can be kept.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Read and write the CSV files with the standard library `csv` module, and keep the assignment's names for variables and functions. pandas is not a runtime dependency.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
|
||||||
|
- The game has no runtime dependencies, so S03 installs nothing and CI is fast.
|
||||||
|
- The game logic is plain functions over `str` and `int`, easy to test without a display.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
|
||||||
|
- The code differs from the lecture's pandas calls, so S02 compares by names and behaviour, not by line.
|
||||||
|
- Reading and writing the CSV takes a few more lines than `read_csv` and `to_csv`.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- [BC-001] - Success criterion 6 (no runtime dependency beyond the standard library) is met without exception.
|
||||||
|
- [SA-001] - Resolves the S02 and S03 conflict in favour of no runtime dependency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../business-case.md
|
||||||
|
[SA-001]: ../stakeholder-analysis.md
|
||||||
|
[bb8730c]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-us-states-game/commit/bb8730c30761f885491abeb764690e0b53d2459b
|
||||||
@@ -15,8 +15,8 @@ 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 |
|
||||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0001 |
|
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0002 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 008 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 |
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# SQA Review Record
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-008 |
|
||||||
|
| CrossReference | [QC-ADR-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-13 | Proposed | Claude (AI) for S01 | AI as stand-in for S01 | Initial review record: Go | [bb8730c] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [ADR-0001]
|
||||||
|
- Checklist used: [QC-ADR-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the author is the same AI)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
### QC-ADR-001
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The ID (`ADR-NNNN`, 4-digit) matches the filename number and is unique and sequential (Mandatory) | Pass | ID ADR-0001 matches file adr-0001-...; first ADR. |
|
||||||
|
| 2 | Context states the problem, the forces (cost, risk, constraints) and the options evaluated (Mandatory) | Pass | Context states problem, constraints from BC-001 and SA-001, and two options. |
|
||||||
|
| 3 | At least two options were considered, or the absence of alternatives is justified (Mandatory) | Pass | Two options: pandas and the csv module. |
|
||||||
|
| 4 | Decision is stated in one or two unhedged sentences and matches one of the evaluated options (Mandatory) | Pass | Decision is two unhedged sentences and equals option 2. |
|
||||||
|
| 5 | Consequences list both Positive and Negative outcomes (Mandatory) | Pass | Positive and Negative lists present. |
|
||||||
|
| 6 | Quality-attribute impacts (performance, security, portability, etc.) are named where the decision affects them (Optional) | Pass | Portability and installation effect named (no runtime dependency). |
|
||||||
|
| 7 | Affected Artifacts are listed as links, and each is also in CrossReference (Mandatory) | Pass | BC-001 and SA-001 are linked and in CrossReference. |
|
||||||
|
| 8 | Status changes are recorded as new Version History rows with a Change summary and Commit link; retained rows are never rewritten (Mandatory) | Pass | One Version History row; to be set Accepted on this Go. |
|
||||||
|
| 9 | A superseded or deprecated ADR names its successor, and the successor links back (Optional) | N-A | Not superseded or deprecated. |
|
||||||
|
| 10 | Written in business or technical language of the domain, with no unexplained abbreviations (Optional) | Pass | Abbreviations spelled out or defined (CSV file name, CI named as continuous integration in BC-001). |
|
||||||
|
| 11 | Status is one of `Proposed`, `Accepted`, `Rejected`, `Deprecated` or `Superseded by ADR-NNNN`, and every Version History row uses only these (Mandatory) | Pass | Status values are from the allowed list. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - ADR-0001 meets every Mandatory criterion.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| None | S01 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md
|
||||||
|
|
||||||
|
[ADR-0001]: ../../adr/adr-0001-csv-module-over-pandas.md
|
||||||
|
[bb8730c]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-us-states-game/commit/bb8730c30761f885491abeb764690e0b53d2459b
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# SQA Review Record
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-009 |
|
||||||
|
| CrossReference | [QC-PY-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-13 | Proposed | Claude (AI) for S01 | AI as stand-in for S01 | Initial review record: Go | [bb8730c] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: source and tests delivered by MIL-002 (branch mil-002-game-core)
|
||||||
|
- Checklist used: [QC-PY-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the author is the same AI)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
### QC-PY-001
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) (Mandatory) | Pass | snake_case functions, PascalCase State; ruff N rules pass. |
|
||||||
|
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes (Mandatory) | Pass | find_state, get_missing_states, save_states_to_learn state purpose; assignment names kept. |
|
||||||
|
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions (Mandatory) | Pass | ruff format and check clean, no suppressions. |
|
||||||
|
| 4 | Every function and method signature is type-annotated, including `-> None` (Mandatory) | Pass | All signatures annotated; mypy --strict passes. |
|
||||||
|
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) (Mandatory) | N-A | No exception handling; a missing file raises FileNotFoundError to the caller. |
|
||||||
|
| 6 | No mutable default arguments and no shadowed builtins (Mandatory) | Pass | Default arguments are immutable (Path values); no builtin shadowed. |
|
||||||
|
| 7 | Files, locks and connections are managed with context managers (Mandatory) | Pass | storage.py opens files with `with`. |
|
||||||
|
| 8 | Public modules, classes and functions have docstrings that say what, not how (Optional) | Pass | Docstrings with Doxygen @param/@return on all public items. |
|
||||||
|
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output (Mandatory) | Pass | No print and no logging in the logic. |
|
||||||
|
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded (Mandatory) | N-A | No Design Class Diagram exists; State is a plain value object. |
|
||||||
|
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network (Mandatory) | Pass | 15 tests named for behaviour, tmp_path for files, no network or order dependence. |
|
||||||
|
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment (Optional) | Pass | mypy strict reports no issues. |
|
||||||
|
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused (Optional) | Pass | No runtime dependencies (ADR-0001). |
|
||||||
|
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike (Mandatory) | Pass | Column names and paths defined once in constants.py; matching rule in find_state only. |
|
||||||
|
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles (Mandatory) | Pass | states.py imports no IO or UI; storage.py imports inward (states, constants); no cycles. |
|
||||||
|
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations (Mandatory) | Pass | states.py has one reason to change (rules), storage.py another (file format); no inheritance. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - Mandatory criteria pass for states.py, storage.py and their tests (ruff, mypy --strict, 17 pytest tests, doxygen without warnings).
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| None | S01 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||||
|
[bb8730c]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-us-states-game/commit/bb8730c30761f885491abeb764690e0b53d2459b
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Pure domain logic of the U.S. States Game: states, answers and misses.
|
||||||
|
|
||||||
|
@file states.py
|
||||||
|
@brief State model, answer matching and the list of missing states.
|
||||||
|
|
||||||
|
Nothing here reads files or touches the screen, so the rules run under test
|
||||||
|
with no display and no file system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class State:
|
||||||
|
"""A U.S. state and the position of its name on the map.
|
||||||
|
|
||||||
|
@param name Name of the state, for example "Ohio".
|
||||||
|
@param x Horizontal map coordinate of the state.
|
||||||
|
@param y Vertical map coordinate of the state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
|
||||||
|
|
||||||
|
def find_state(answer: str, states: Iterable[State]) -> State | None:
|
||||||
|
"""Find the state the player named, ignoring case and surrounding spaces.
|
||||||
|
|
||||||
|
@param answer Text the player typed.
|
||||||
|
@param states The states to search.
|
||||||
|
@return The matching state, or None if the answer matches no state.
|
||||||
|
"""
|
||||||
|
wanted = answer.strip().casefold()
|
||||||
|
if not wanted:
|
||||||
|
return None
|
||||||
|
for state in states:
|
||||||
|
if state.name.casefold() == wanted:
|
||||||
|
return state
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_missing_states(
|
||||||
|
states: Iterable[State], guessed_states: Iterable[str]
|
||||||
|
) -> list[str]:
|
||||||
|
"""List the names of the states the player did not guess.
|
||||||
|
|
||||||
|
@param states All states of the game, in the order to report them.
|
||||||
|
@param guessed_states Names of the states the player guessed.
|
||||||
|
@return Names of the states not guessed, in the order of `states`.
|
||||||
|
"""
|
||||||
|
guessed = set(guessed_states)
|
||||||
|
return [state.name for state in states if state.name not in guessed]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""CSV input and output of the U.S. States Game, using the csv module.
|
||||||
|
|
||||||
|
@file storage.py
|
||||||
|
@brief Loads the states from CSV and saves the states to learn.
|
||||||
|
|
||||||
|
Uses the standard library `csv` module, not pandas (see ADR-0001).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from us_states_game.constants import (
|
||||||
|
STATE_COLUMN,
|
||||||
|
STATES_CSV_PATH,
|
||||||
|
STATES_TO_LEARN_FILENAME,
|
||||||
|
X_COLUMN,
|
||||||
|
Y_COLUMN,
|
||||||
|
)
|
||||||
|
from us_states_game.states import State
|
||||||
|
|
||||||
|
|
||||||
|
def load_states(path: Path = STATES_CSV_PATH) -> list[State]:
|
||||||
|
"""Read the states and their map coordinates from a CSV file.
|
||||||
|
|
||||||
|
@param path CSV file with the state, x and y columns.
|
||||||
|
@return The states, in file order.
|
||||||
|
"""
|
||||||
|
with path.open(newline="", encoding="utf-8") as file:
|
||||||
|
return [
|
||||||
|
State(row[STATE_COLUMN], int(row[X_COLUMN]), int(row[Y_COLUMN]))
|
||||||
|
for row in csv.DictReader(file)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def save_states_to_learn(
|
||||||
|
missing_states: Iterable[str],
|
||||||
|
path: Path = Path(STATES_TO_LEARN_FILENAME),
|
||||||
|
) -> None:
|
||||||
|
"""Write the missing states to a one-column CSV file.
|
||||||
|
|
||||||
|
@param missing_states Names of the states the player missed.
|
||||||
|
@param path File to write; an existing file is replaced.
|
||||||
|
"""
|
||||||
|
with path.open("w", newline="", encoding="utf-8") as file:
|
||||||
|
writer = csv.writer(file)
|
||||||
|
writer.writerow([STATE_COLUMN])
|
||||||
|
writer.writerows([name] for name in missing_states)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Tests of the pure game logic in states.py."""
|
||||||
|
|
||||||
|
from us_states_game.states import State, find_state, get_missing_states
|
||||||
|
|
||||||
|
STATES = [State("Alabama", 139, -77), State("Ohio", 100, 20), State("Texas", -50, -120)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_state_matches_exact_name() -> None:
|
||||||
|
assert find_state("Ohio", STATES) == State("Ohio", 100, 20)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_state_ignores_case() -> None:
|
||||||
|
assert find_state("oHiO", STATES) == State("Ohio", 100, 20)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_state_ignores_surrounding_whitespace() -> None:
|
||||||
|
assert find_state(" ohio \n", STATES) == State("Ohio", 100, 20)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_state_returns_none_for_unknown_answer() -> None:
|
||||||
|
assert find_state("Atlantis", STATES) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_state_returns_none_for_empty_answer() -> None:
|
||||||
|
assert find_state("", STATES) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_state_returns_none_for_blank_answer() -> None:
|
||||||
|
assert find_state(" ", STATES) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_missing_states_keeps_order_of_states() -> None:
|
||||||
|
assert get_missing_states(STATES, ["Ohio"]) == ["Alabama", "Texas"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_missing_states_is_empty_when_all_guessed() -> None:
|
||||||
|
assert get_missing_states(STATES, ["Texas", "Ohio", "Alabama"]) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_missing_states_returns_all_when_none_guessed() -> None:
|
||||||
|
assert get_missing_states(STATES, []) == ["Alabama", "Ohio", "Texas"]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Tests of the CSV input and output in storage.py."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from us_states_game.constants import NUMBER_OF_STATES, STATE_COLUMN
|
||||||
|
from us_states_game.states import State, get_missing_states
|
||||||
|
from us_states_game.storage import load_states, save_states_to_learn
|
||||||
|
|
||||||
|
|
||||||
|
def _read_rows(path: Path) -> list[list[str]]:
|
||||||
|
with path.open(newline="", encoding="utf-8") as file:
|
||||||
|
return list(csv.reader(file))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_states_returns_all_states() -> None:
|
||||||
|
assert len(load_states()) == NUMBER_OF_STATES
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_states_reads_alabama_coordinates() -> None:
|
||||||
|
assert load_states()[0] == State("Alabama", 139, -77)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_states_reads_custom_path(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "states.csv"
|
||||||
|
path.write_text("state,x,y\nOhio,10,-20\n", encoding="utf-8")
|
||||||
|
assert load_states(path) == [State("Ohio", 10, -20)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_states_to_learn_writes_header_and_states(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "learn.csv"
|
||||||
|
save_states_to_learn(["Ohio", "Texas"], path)
|
||||||
|
assert _read_rows(path) == [[STATE_COLUMN], ["Ohio"], ["Texas"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_states_to_learn_writes_only_header_when_nothing_missing(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
path = tmp_path / "learn.csv"
|
||||||
|
save_states_to_learn([], path)
|
||||||
|
assert _read_rows(path) == [[STATE_COLUMN]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_states_to_learn_lists_47_rows_after_three_guesses(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
path = tmp_path / "learn.csv"
|
||||||
|
missing = get_missing_states(load_states(), ["Alabama", "Ohio", "Texas"])
|
||||||
|
save_states_to_learn(missing, path)
|
||||||
|
assert len(_read_rows(path)) - 1 == NUMBER_OF_STATES - 3
|
||||||
Reference in New Issue
Block a user