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:
@@ -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)
|
||||
Reference in New Issue
Block a user