Add game core logic, CSV storage, tests and ADR-0001 (MIL-002)

Task: MIL-002#1
Task: MIL-002#2
Task: MIL-002#3
Task: MIL-002#4
Task: MIL-002#5
This commit is contained in:
2026-10-13 15:48:08 +08:00
parent 048a6ba91d
commit bb8730c307
8 changed files with 363 additions and 2 deletions
+54
View File
@@ -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]
+48
View File
@@ -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)