feat: add supplemental theme catalog tooling, additional theme selection, and custom theme selection

This commit is contained in:
matt 2025-10-03 10:43:24 -07:00
parent 3a1b011dbc
commit 9428e09cef
39 changed files with 3643 additions and 198 deletions

View file

@ -0,0 +1,92 @@
from __future__ import annotations
import time
import pytest
from code.deck_builder.theme_catalog_loader import ThemeCatalogEntry
from code.deck_builder.theme_matcher import (
ACCEPT_MATCH_THRESHOLD,
SUGGEST_MATCH_THRESHOLD,
ThemeMatcher,
normalize_theme,
)
@pytest.fixture()
def sample_entries() -> list[ThemeCatalogEntry]:
themes = [
"Aristocrats",
"Sacrifice Matters",
"Life Gain",
"Token Swarm",
"Control",
"Superfriends",
"Spellslinger",
"Artifact Tokens",
"Treasure Storm",
"Graveyard Loops",
]
return [ThemeCatalogEntry(theme=theme, commander_count=0, card_count=0) for theme in themes]
def test_normalize_theme_collapses_spaces() -> None:
assert normalize_theme(" Life Gain \t") == "life gain"
def test_exact_match_case_insensitive(sample_entries: list[ThemeCatalogEntry]) -> None:
matcher = ThemeMatcher(sample_entries)
result = matcher.resolve("aristocrats")
assert result.matched_theme == "Aristocrats"
assert result.score == pytest.approx(100.0)
assert result.reason == "high_confidence"
def test_minor_typo_accepts_with_high_score(sample_entries: list[ThemeCatalogEntry]) -> None:
matcher = ThemeMatcher(sample_entries)
result = matcher.resolve("aristrocrats")
assert result.matched_theme == "Aristocrats"
assert result.score >= ACCEPT_MATCH_THRESHOLD
assert result.reason in {"high_confidence", "accepted_confidence"}
def test_multi_typo_only_suggests(sample_entries: list[ThemeCatalogEntry]) -> None:
matcher = ThemeMatcher(sample_entries)
result = matcher.resolve("arzstrcrats")
assert result.matched_theme is None
assert result.score >= SUGGEST_MATCH_THRESHOLD
assert result.reason == "suggestions"
assert any(s.theme == "Aristocrats" for s in result.suggestions)
def test_no_match_returns_empty(sample_entries: list[ThemeCatalogEntry]) -> None:
matcher = ThemeMatcher(sample_entries)
result = matcher.resolve("planeship")
assert result.matched_theme is None
assert result.suggestions == []
assert result.reason in {"no_candidates", "no_match"}
def test_short_input_requires_exact(sample_entries: list[ThemeCatalogEntry]) -> None:
matcher = ThemeMatcher(sample_entries)
result = matcher.resolve("ar")
assert result.matched_theme is None
assert result.reason == "input_too_short"
result_exact = matcher.resolve("lo")
assert result_exact.matched_theme is None
def test_resolution_speed(sample_entries: list[ThemeCatalogEntry]) -> None:
many_entries = [
ThemeCatalogEntry(theme=f"Theme {i}", commander_count=0, card_count=0) for i in range(400)
]
matcher = ThemeMatcher(many_entries)
matcher.resolve("theme 42")
start = time.perf_counter()
for _ in range(20):
matcher.resolve("theme 123")
duration = time.perf_counter() - start
# Observed ~0.03s per resolution (<=0.65s for 20 resolves) on dev machine (2025-10-02).
assert duration < 0.7