Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions backend/app/routers/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ def _resolve_trivia(card: dict, cards: list[dict]) -> str | None:
return None


@lru_cache(maxsize=1)
def _card_histories() -> dict[str, list]:
"""Per-card game-patch history scraped from the STS2 wiki (CC BY-SA 4.0)
by tools/wiki_card_history.py. English-only; empty if the file's absent."""
try:
with open(DATA_DIR / "card_history.json", "r", encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError):
return {}
cards = data.get("cards") if isinstance(data, dict) else None
return cards if isinstance(cards, dict) else {}


def _matches_cost(card: dict, want: str) -> bool:
if want == "x":
return bool(card.get("is_x_cost"))
Expand Down Expand Up @@ -136,6 +149,19 @@ def get_cards(
return cards


@router.get("/{card_id}/history")
def get_card_history(card_id: str):
"""Update history for one card as documented on the wiki, newest first.
404s when the wiki has no table for the card yet, so the frontend can
fall back to the site-changelog timeline."""
entries = _card_histories().get(card_id.upper())
if not entries:
raise HTTPException(
status_code=404, detail=f"No update history for '{card_id}'"
)
return entries


@router.get("/{card_id}", response_model=Card)
def get_card(request: Request, card_id: str, lang: str = Depends(get_lang)):
cards = load_cards(lang)
Expand Down
51 changes: 51 additions & 0 deletions backend/tests/test_card_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The per-card history endpoint serves the wiki-scraped update tables in
data/card_history.json; unknown cards 404 so the frontend can fall back to
the site-changelog timeline."""

import pytest
from fastapi import HTTPException

from app.routers.cards import _card_histories, get_card_history


def test_history_file_loads_and_is_well_formed():
histories = _card_histories()
assert len(histories) > 400
for card_id, entries in list(histories.items())[:50]:
assert card_id == card_id.upper()
assert entries
for entry in entries:
assert set(entry) == {"version", "type", "date", "changes"}
assert isinstance(entry["changes"], list)
assert all(isinstance(c, str) and c for c in entry["changes"])


def test_dated_entries_are_newest_first():
histories = _card_histories()
checked = 0
for entries in histories.values():
dates = [e["date"] for e in entries if e["date"]]
if len(dates) > 1:
assert dates == sorted(dates, reverse=True)
checked += 1
assert checked > 50


def test_known_card_returns_its_entries():
histories = _card_histories()
card_id = next(iter(histories))
assert get_card_history(card_id.lower()) == histories[card_id]


def test_unknown_card_404s():
with pytest.raises(HTTPException) as exc:
get_card_history("NOT_A_REAL_CARD")
assert exc.value.status_code == 404


def test_starter_variants_have_history():
# Duplicate-named starters resolve to per-character wiki pages
# (e.g. "Strike (Ironclad)"), which regressed once already.
histories = _card_histories()
assert "STRIKE_IRONCLAD" in histories
assert "DEFEND_IRONCLAD" in histories
Loading
Loading