Add Turtle screen, game loop and entry point (MIL-003)
Task: MIL-003#1 Task: MIL-003#2 Task: MIL-003#3 Task: MIL-003#4 Task: MIL-003#5 Task: MIL-003#6
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Tests of the guess loop and the end-of-game export in game.py."""
|
||||
|
||||
import csv
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from us_states_game.constants import ANSWER_PROMPT, NUMBER_OF_STATES
|
||||
from us_states_game.game import play, run_game
|
||||
from us_states_game.states import State
|
||||
|
||||
STATES = [State("Alabama", 139, -77), State("Ohio", 100, 20), State("Texas", -50, -120)]
|
||||
ALL_STATES = [State(f"State {n}", n, -n) for n in range(NUMBER_OF_STATES)]
|
||||
|
||||
|
||||
class FakeScreen:
|
||||
"""Records what the loop asks and writes, and returns scripted answers."""
|
||||
|
||||
def __init__(self, answers: Sequence[str | None]) -> None:
|
||||
self._answers = iter(answers)
|
||||
self.titles: list[str] = []
|
||||
self.prompts: list[str] = []
|
||||
self.written: list[State] = []
|
||||
|
||||
def ask_state_name(self, title: str, prompt: str) -> str | None:
|
||||
self.titles.append(title)
|
||||
self.prompts.append(prompt)
|
||||
return next(self._answers)
|
||||
|
||||
def write_state(self, state: State) -> None:
|
||||
self.written.append(state)
|
||||
|
||||
|
||||
def read_names(path: Path) -> list[str]:
|
||||
with path.open(newline="", encoding="utf-8") as file:
|
||||
return [row[0] for row in csv.reader(file)]
|
||||
|
||||
|
||||
def test_play_writes_correct_guess_at_its_coordinates() -> None:
|
||||
screen = FakeScreen(["Ohio", "exit"])
|
||||
assert play(screen, STATES) == ["Ohio"]
|
||||
assert screen.written == [State("Ohio", 100, 20)]
|
||||
|
||||
|
||||
def test_play_increments_counter_in_title_after_correct_guess() -> None:
|
||||
screen = FakeScreen(["ohio", "texas", "exit"])
|
||||
play(screen, STATES)
|
||||
assert screen.titles == [
|
||||
"0/50 States Correct",
|
||||
"1/50 States Correct",
|
||||
"2/50 States Correct",
|
||||
]
|
||||
assert set(screen.prompts) == {ANSWER_PROMPT}
|
||||
|
||||
|
||||
def test_play_uses_canonical_name_for_case_insensitive_guess() -> None:
|
||||
assert play(FakeScreen(["oHiO", "exit"]), STATES) == ["Ohio"]
|
||||
|
||||
|
||||
def test_play_asks_again_without_writing_for_wrong_guess() -> None:
|
||||
screen = FakeScreen(["Atlantis", "exit"])
|
||||
assert play(screen, STATES) == []
|
||||
assert screen.written == []
|
||||
assert screen.titles == ["0/50 States Correct", "0/50 States Correct"]
|
||||
|
||||
|
||||
def test_play_counts_duplicate_guess_once() -> None:
|
||||
screen = FakeScreen(["Ohio", "ohio", " OHIO ", "exit"])
|
||||
assert play(screen, STATES) == ["Ohio"]
|
||||
assert len(screen.written) == 1
|
||||
assert screen.titles[-1] == "1/50 States Correct"
|
||||
|
||||
|
||||
def test_play_ends_on_exit_command() -> None:
|
||||
screen = FakeScreen(["exit"])
|
||||
assert play(screen, STATES) == []
|
||||
assert len(screen.titles) == 1
|
||||
|
||||
|
||||
def test_play_ends_on_exit_command_in_any_case() -> None:
|
||||
assert play(FakeScreen(["Ohio", "EXIT"]), STATES) == ["Ohio"]
|
||||
|
||||
|
||||
def test_play_ends_when_player_cancels() -> None:
|
||||
assert play(FakeScreen(["Ohio", None]), STATES) == ["Ohio"]
|
||||
|
||||
|
||||
def test_play_ends_without_asking_again_when_all_states_are_guessed() -> None:
|
||||
screen = FakeScreen([state.name for state in ALL_STATES])
|
||||
assert len(play(screen, ALL_STATES)) == NUMBER_OF_STATES
|
||||
assert len(screen.titles) == NUMBER_OF_STATES
|
||||
|
||||
|
||||
def test_run_game_writes_exactly_the_missing_states(tmp_path: Path) -> None:
|
||||
output = tmp_path / "states_to_learn.csv"
|
||||
missing = run_game(FakeScreen(["Ohio", "exit"]), STATES, output)
|
||||
assert missing == ["Alabama", "Texas"]
|
||||
assert read_names(output) == ["state", "Alabama", "Texas"]
|
||||
|
||||
|
||||
def test_run_game_writes_header_only_when_all_states_guessed(tmp_path: Path) -> None:
|
||||
output = tmp_path / "states_to_learn.csv"
|
||||
screen = FakeScreen([state.name for state in ALL_STATES])
|
||||
assert run_game(screen, ALL_STATES, output) == []
|
||||
assert read_names(output) == ["state"]
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Tests of the entry point in main.py, with a fake screen."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from us_states_game import main as main_module
|
||||
from us_states_game.states import State
|
||||
from us_states_game.storage import load_states
|
||||
|
||||
|
||||
class FakeTurtleScreen:
|
||||
"""Stands in for TurtleScreen: scripted answers, no display."""
|
||||
|
||||
instances: list["FakeTurtleScreen"] = []
|
||||
answers: Sequence[str | None] = []
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._answers = iter(self.answers)
|
||||
self.written: list[State] = []
|
||||
self.coordinates_enabled = False
|
||||
self.closed = False
|
||||
FakeTurtleScreen.instances.append(self)
|
||||
|
||||
def ask_state_name(self, title: str, prompt: str) -> str | None:
|
||||
return next(self._answers)
|
||||
|
||||
def write_state(self, state: State) -> None:
|
||||
self.written.append(state)
|
||||
|
||||
def enable_click_coordinates(self) -> None:
|
||||
self.coordinates_enabled = True
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_screen(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> type[FakeTurtleScreen]:
|
||||
FakeTurtleScreen.instances = []
|
||||
monkeypatch.setattr(main_module, "TurtleScreen", FakeTurtleScreen)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
return FakeTurtleScreen
|
||||
|
||||
|
||||
def test_main_writes_states_to_learn_and_returns_zero(
|
||||
fake_screen: type[FakeTurtleScreen], tmp_path: Path
|
||||
) -> None:
|
||||
fake_screen.answers = ["Ohio", "texas", "exit"]
|
||||
assert main_module.main([]) == 0
|
||||
lines = (tmp_path / "states_to_learn.csv").read_text(encoding="utf-8").splitlines()
|
||||
expected = [s.name for s in load_states() if s.name not in {"Ohio", "Texas"}]
|
||||
assert lines == ["state", *expected]
|
||||
assert len(expected) == 48
|
||||
|
||||
|
||||
def test_main_closes_screen_after_game(fake_screen: type[FakeTurtleScreen]) -> None:
|
||||
fake_screen.answers = ["exit"]
|
||||
main_module.main([])
|
||||
assert fake_screen.instances[0].closed
|
||||
|
||||
|
||||
def test_main_enables_click_coordinates_with_flag(
|
||||
fake_screen: type[FakeTurtleScreen],
|
||||
) -> None:
|
||||
fake_screen.answers = ["exit"]
|
||||
main_module.main(["--show-coordinates"])
|
||||
assert fake_screen.instances[0].coordinates_enabled
|
||||
|
||||
|
||||
def test_main_leaves_click_coordinates_off_by_default(
|
||||
fake_screen: type[FakeTurtleScreen],
|
||||
) -> None:
|
||||
fake_screen.answers = ["exit"]
|
||||
main_module.main([])
|
||||
assert not fake_screen.instances[0].coordinates_enabled
|
||||
|
||||
|
||||
def test_game_and_states_modules_do_not_import_turtle() -> None:
|
||||
code = (
|
||||
"import sys, us_states_game.game, us_states_game.states, "
|
||||
"us_states_game.storage; "
|
||||
"sys.exit(1 if 'turtle' in sys.modules else 0)"
|
||||
)
|
||||
result = subprocess.run([sys.executable, "-c", code], check=False)
|
||||
assert result.returncode == 0
|
||||
Reference in New Issue
Block a user