Progressive title truncation, publisher tiebreak, thematic-year fix
Long transcribed box titles that defeat search now retry with shorter heads (pre-separator, pre-"Game ..." descriptor, first-two-words from the pre-subtitle part) matched exact-only against the head — fuzzy thresholds stay untouched. Tie-breaks gain a publisher pick: when the box showed a publisher and exactly one exact-named candidate is from that publisher, it wins (SPI's Sorcerer 1975 now beats the more-owned White Wizard Sorcerer 2019). The extract prompt excludes thematic/ subject years from year_hint; re-extracting Flat Top's photo drops the bogus 1942, and a regression test pins that a wrong year can never drive version selection. Re-extraction also drifted two transcriptions (DUNGEON!, and Herbaceous misread as "Hebarceos") — fixtures added; the misread demos review's re-search rescue in the end-to-end run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+18
-18
@@ -249,26 +249,26 @@
|
|||||||
"title_normalized": "exploding kittens"
|
"title_normalized": "exploding kittens"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title_raw": "Herbaceous",
|
"title_raw": "Hebarceos",
|
||||||
"confidence": "high",
|
"confidence": "low",
|
||||||
"publisher_hint": "",
|
"publisher_hint": "",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "",
|
||||||
"art_notes": "Colorful spine with illustrated herb/plant imagery, 'A player's game of...' text visible",
|
"art_notes": "yellow box with floral/plant illustration",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
"title_normalized": "herbaceous"
|
"title_normalized": "hebarceos"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title_raw": "Wiz-War",
|
"title_raw": "Wiz-War",
|
||||||
"confidence": "high",
|
"confidence": "medium",
|
||||||
"publisher_hint": "",
|
"publisher_hint": "",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Dark blue/black box, partial text 'board game of...battle and treasures'",
|
"art_notes": "dark blue box, text 'board game of...battle and treasures'",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Bright pink/magenta box with striped pattern, tagline 'Doodle your way to disaster'",
|
"art_notes": "pink and purple striped box, subtitle 'Doodle your way to disaster'",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
@@ -292,9 +292,9 @@
|
|||||||
"confidence": "high",
|
"confidence": "high",
|
||||||
"publisher_hint": "",
|
"publisher_hint": "",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": 1942,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Blue spine, subtitle 'A GAME of Carrier Battles in the Pacific • 1942'",
|
"art_notes": "light blue spine, subtitle 'A GAME of Carrier Battles in the Pacific • 1942'",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
@@ -303,11 +303,11 @@
|
|||||||
{
|
{
|
||||||
"title_raw": "Diplomacy",
|
"title_raw": "Diplomacy",
|
||||||
"confidence": "high",
|
"confidence": "high",
|
||||||
"publisher_hint": "Avalon Hill",
|
"publisher_hint": "",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Black spine, subtitle 'The Exciting Game of International Intrigue - For 2 to 7 Players'",
|
"art_notes": "black spine, subtitle 'The Exciting Game of International Intrigue, For 2 to 7 Players'",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
@@ -316,11 +316,11 @@
|
|||||||
{
|
{
|
||||||
"title_raw": "Age of Renaissance",
|
"title_raw": "Age of Renaissance",
|
||||||
"confidence": "high",
|
"confidence": "high",
|
||||||
"publisher_hint": "Avalon Hill",
|
"publisher_hint": "AH (Avalon Hill)",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Tan/brown spine with gold diamond emblem, subtitle about building dominant civilization of Europe",
|
"art_notes": "tan/brown spine with gold emblem, subtitle about building dominant civilization of Europe",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
@@ -329,24 +329,24 @@
|
|||||||
{
|
{
|
||||||
"title_raw": "History of the World",
|
"title_raw": "History of the World",
|
||||||
"confidence": "high",
|
"confidence": "high",
|
||||||
"publisher_hint": "Avalon Hill",
|
"publisher_hint": "AH (Avalon Hill)",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Blue/purple spine, subtitle 'Game of World Conquest'",
|
"art_notes": "light blue/lavender spine, subtitle 'Game of World Conquest'",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
"title_normalized": "history of world"
|
"title_normalized": "history of world"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title_raw": "Dungeon!",
|
"title_raw": "DUNGEON!",
|
||||||
"confidence": "high",
|
"confidence": "high",
|
||||||
"publisher_hint": "TSR",
|
"publisher_hint": "TSR",
|
||||||
"edition_hint": "",
|
"edition_hint": "",
|
||||||
"year_hint": null,
|
"year_hint": null,
|
||||||
"language_hint": "English",
|
"language_hint": "English",
|
||||||
"art_notes": "Purple box bottom with fantasy dungeon-themed artwork, visible at base of stack",
|
"art_notes": "purple/dark spine with stone dungeon texture illustration",
|
||||||
"source_photos": [
|
"source_photos": [
|
||||||
"IMG_4502.jpeg"
|
"IMG_4502.jpeg"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ SEARCHES: dict[str, list[tuple]] = {
|
|||||||
(172242, "Exploding Kittens: NSFW Edition", 2015, BG, "primary"),
|
(172242, "Exploding Kittens: NSFW Edition", 2015, BG, "primary"),
|
||||||
],
|
],
|
||||||
"Herbaceous": [(195314, "Herbaceous", 2017, BG, "primary")],
|
"Herbaceous": [(195314, "Herbaceous", 2017, BG, "primary")],
|
||||||
|
# vision misread of Herbaceous on one run — search finds nothing;
|
||||||
|
# review's free-text re-search ("f Herbaceous") rescues it
|
||||||
|
"Hebarceos": [],
|
||||||
"Wiz-War": [
|
"Wiz-War": [
|
||||||
(1218, "Wiz-War", 1983, BG, "primary"),
|
(1218, "Wiz-War", 1983, BG, "primary"),
|
||||||
(104710, "Wiz-War", 2012, BG, "alternate"), # primary: Wiz-War (Eighth Edition)
|
(104710, "Wiz-War", 2012, BG, "alternate"), # primary: Wiz-War (Eighth Edition)
|
||||||
@@ -94,6 +97,7 @@ SEARCHES: dict[str, list[tuple]] = {
|
|||||||
"Age of Renaissance": [(26, "Age of Renaissance", 1996, BG, "primary")],
|
"Age of Renaissance": [(26, "Age of Renaissance", 1996, BG, "primary")],
|
||||||
"History of the World": [(224, "History of the World", 1991, BG, "primary")],
|
"History of the World": [(224, "History of the World", 1991, BG, "primary")],
|
||||||
"Dungeon!": [(1339, "Dungeon!", 1975, BG, "primary")],
|
"Dungeon!": [(1339, "Dungeon!", 1975, BG, "primary")],
|
||||||
|
"DUNGEON!": [(1339, "Dungeon!", 1975, BG, "primary")], # spelling drift
|
||||||
}
|
}
|
||||||
|
|
||||||
# ids-param -> [(bgg_id, name, year, type, owned, rank, [publishers])]
|
# ids-param -> [(bgg_id, name, year, type, owned, rank, [publishers])]
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ SEARCHES = {
|
|||||||
+ search_item(205398, "Citadels", 2016, "boardgame")
|
+ search_item(205398, "Citadels", 2016, "boardgame")
|
||||||
),
|
),
|
||||||
"Blorvath: Quest of the Zzyzx": "",
|
"Blorvath: Quest of the Zzyzx": "",
|
||||||
|
"Blorvath": "", # truncation-retry head of the nonsense title
|
||||||
}
|
}
|
||||||
|
|
||||||
THINGS = {
|
THINGS = {
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ Respond with ONLY a JSON array, no prose, where each element is:
|
|||||||
"publisher_hint": "publisher name or logo if legible, else null",
|
"publisher_hint": "publisher name or logo if legible, else null",
|
||||||
"edition_hint": "edition wording if visible ('2nd Edition', 'Deluxe',
|
"edition_hint": "edition wording if visible ('2nd Edition', 'Deluxe',
|
||||||
'Big Box', anniversary marks), else null",
|
'Big Box', anniversary marks), else null",
|
||||||
"year_hint": copyright/print year as an integer if legible, else null,
|
"year_hint": copyright or publication year as an integer, ONLY if printed
|
||||||
|
as publishing info (copyright line, edition year). NEVER use
|
||||||
|
a year that is part of the game's title, theme, or subject
|
||||||
|
matter — a wargame about 1942 is not published in 1942.
|
||||||
|
When unsure whether a year is thematic, use null,
|
||||||
"language_hint": "language of the box text if determinable, else null",
|
"language_hint": "language of the box text if determinable, else null",
|
||||||
"art_notes": "distinctive box-art notes (colorway, artwork style) that
|
"art_notes": "distinctive box-art notes (colorway, artwork style) that
|
||||||
could identify the edition, else null"
|
could identify the edition, else null"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class ThingDetails:
|
|||||||
type: str
|
type: str
|
||||||
owned: int | None = None
|
owned: int | None = None
|
||||||
rank: int | None = None
|
rank: int | None = None
|
||||||
|
publishers: tuple[str, ...] = field(default=())
|
||||||
versions: tuple[GameVersion, ...] = field(default=())
|
versions: tuple[GameVersion, ...] = field(default=())
|
||||||
|
|
||||||
|
|
||||||
@@ -124,6 +125,10 @@ def parse_things(xml_text: str) -> list[ThingDetails]:
|
|||||||
type=item.get("type", "boardgame"),
|
type=item.get("type", "boardgame"),
|
||||||
owned=_attr_int(item.find(".//ratings/owned")),
|
owned=_attr_int(item.find(".//ratings/owned")),
|
||||||
rank=_attr_int(rank_elem),
|
rank=_attr_int(rank_elem),
|
||||||
|
publishers=tuple(
|
||||||
|
link.get("value", "")
|
||||||
|
for link in item.findall("link[@type='boardgamepublisher']")
|
||||||
|
),
|
||||||
versions=tuple(versions),
|
versions=tuple(versions),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+75
-6
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ class Candidate:
|
|||||||
fuzzy: float
|
fuzzy: float
|
||||||
owned: int | None = None
|
owned: int | None = None
|
||||||
rank: int | None = None
|
rank: int | None = None
|
||||||
|
publishers: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
def as_json(self) -> dict:
|
def as_json(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -151,15 +153,55 @@ def load_titles(path: Path) -> list[TitleEntry]:
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def _plausible_candidates(client: BGGClient, entry: TitleEntry) -> list[Candidate]:
|
_SEPARATORS = (" — ", " – ", " - ", ": ", "; ")
|
||||||
"""Search BGG and keep exact-normalized or fuzzy>=90 candidates, one per id."""
|
_GAME_WORD = re.compile(r"\s+(?:the\s+|a\s+)?game\b", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _truncation_heads(title_raw: str) -> list[str]:
|
||||||
|
"""Progressively shorter heads for box titles whose printed subtitle
|
||||||
|
defeats search ("CIVILIZATION Game of the Heroic Age - ..."): text
|
||||||
|
before the first subtitle separator, before a "(The) Game ..."
|
||||||
|
descriptor, then the first two words as a last resort."""
|
||||||
|
heads: list[str] = []
|
||||||
|
sep_head = next(
|
||||||
|
(title_raw.split(sep)[0] for sep in _SEPARATORS if sep in title_raw), None
|
||||||
|
)
|
||||||
|
if sep_head:
|
||||||
|
heads.append(sep_head)
|
||||||
|
match = _GAME_WORD.search(title_raw)
|
||||||
|
if match and match.start() > 0:
|
||||||
|
heads.append(title_raw[: match.start()])
|
||||||
|
# last resort: first two words — of the pre-subtitle part, so a title
|
||||||
|
# like "Blorvath: Quest of the Zzyzx" never yields "Blorvath: Quest"
|
||||||
|
words = (sep_head or title_raw).split()
|
||||||
|
if len(words) > 2:
|
||||||
|
heads.append(" ".join(words[:2]))
|
||||||
|
|
||||||
|
seen: set[str] = {normalize_title(title_raw)}
|
||||||
|
unique: list[str] = []
|
||||||
|
for head in heads:
|
||||||
|
norm = normalize_title(head)
|
||||||
|
if norm and norm not in seen:
|
||||||
|
seen.add(norm)
|
||||||
|
unique.append(head)
|
||||||
|
return unique[:3]
|
||||||
|
|
||||||
|
|
||||||
|
def _plausible_candidates(
|
||||||
|
client: BGGClient, entry: TitleEntry, query: str, head_normalized: str = ""
|
||||||
|
) -> list[Candidate]:
|
||||||
|
"""Search BGG and keep plausible candidates, one per id: exact-normalized
|
||||||
|
or fuzzy>=90 against the FULL title, or — on truncated retries — exact
|
||||||
|
(only exact: truncation must stay conservative) against the head."""
|
||||||
by_id: dict[int, Candidate] = {}
|
by_id: dict[int, Candidate] = {}
|
||||||
for result in client.search(entry.title_raw):
|
for result in client.search(query):
|
||||||
norm = normalize_title(result.name)
|
norm = normalize_title(result.name)
|
||||||
exact = norm == entry.title_normalized
|
exact = norm == entry.title_normalized
|
||||||
fuzzy = fuzz.token_sort_ratio(norm, entry.title_normalized)
|
fuzzy = fuzz.token_sort_ratio(norm, entry.title_normalized)
|
||||||
if not exact and fuzzy < FUZZY_THRESHOLD:
|
if not exact and fuzzy < FUZZY_THRESHOLD:
|
||||||
continue
|
if not (head_normalized and norm == head_normalized):
|
||||||
|
continue
|
||||||
|
exact = True # head-exact counts as strong, nothing weaker does
|
||||||
candidate = Candidate(
|
candidate = Candidate(
|
||||||
bgg_id=result.bgg_id,
|
bgg_id=result.bgg_id,
|
||||||
name=result.name,
|
name=result.name,
|
||||||
@@ -194,8 +236,9 @@ def _classify(client: BGGClient, entry: TitleEntry, cands: list[Candidate]) -> M
|
|||||||
if c.bgg_id in stats:
|
if c.bgg_id in stats:
|
||||||
c.owned = stats[c.bgg_id].owned
|
c.owned = stats[c.bgg_id].owned
|
||||||
c.rank = stats[c.bgg_id].rank
|
c.rank = stats[c.bgg_id].rank
|
||||||
|
c.publishers = list(stats[c.bgg_id].publishers)
|
||||||
row.candidates = top
|
row.candidates = top
|
||||||
chosen = _dominant(top)
|
chosen = _publisher_pick(entry, top) or _dominant(top)
|
||||||
if chosen is None:
|
if chosen is None:
|
||||||
row.match_status = "ambiguous"
|
row.match_status = "ambiguous"
|
||||||
return row
|
return row
|
||||||
@@ -209,6 +252,23 @@ def _classify(client: BGGClient, entry: TitleEntry, cands: list[Candidate]) -> M
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _publisher_pick(entry: TitleEntry, top: list[Candidate]) -> Candidate | None:
|
||||||
|
"""When a publisher was legible on the box, and exactly one exact-named
|
||||||
|
candidate is from that publisher, that's the game."""
|
||||||
|
if not entry.publisher_hint:
|
||||||
|
return None
|
||||||
|
hint = normalize_title(entry.publisher_hint)
|
||||||
|
matches = [
|
||||||
|
c
|
||||||
|
for c in top
|
||||||
|
if c.exact
|
||||||
|
and any(
|
||||||
|
fuzz.partial_ratio(hint, normalize_title(p)) >= 85 for p in c.publishers
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return matches[0] if len(matches) == 1 else None
|
||||||
|
|
||||||
|
|
||||||
def _dominant(top: list[Candidate]) -> Candidate | None:
|
def _dominant(top: list[Candidate]) -> Candidate | None:
|
||||||
"""The single clear winner among plausible candidates, if any.
|
"""The single clear winner among plausible candidates, if any.
|
||||||
|
|
||||||
@@ -291,7 +351,16 @@ def _resolve_version(client: BGGClient, entry: TitleEntry, row: MatchRow) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow:
|
def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow:
|
||||||
row = _classify(client, entry, _plausible_candidates(client, entry))
|
cands = _plausible_candidates(client, entry, entry.title_raw)
|
||||||
|
if not cands:
|
||||||
|
# Long transcribed box titles ("CIVILIZATION Game of the Heroic
|
||||||
|
# Age - ...") defeat search: retry with progressively shorter heads
|
||||||
|
# instead of loosening the fuzzy threshold.
|
||||||
|
for head in _truncation_heads(entry.title_raw):
|
||||||
|
cands = _plausible_candidates(client, entry, head, normalize_title(head))
|
||||||
|
if cands:
|
||||||
|
break
|
||||||
|
row = _classify(client, entry, cands)
|
||||||
if row.match_status == "auto":
|
if row.match_status == "auto":
|
||||||
_resolve_version(client, entry, row)
|
_resolve_version(client, entry, row)
|
||||||
return row
|
return row
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<items total="0"></items>
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<items total="1"><item type="boardgame" id="1339"><name type="primary" value="Dungeon!"/><yearpublished value="1975"/></item></items>
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<items total="0"></items>
|
||||||
@@ -187,3 +187,110 @@ def test_score_version_no_overlap():
|
|||||||
languages=("English",),
|
languages=("English",),
|
||||||
)
|
)
|
||||||
assert _score_version(entry, version) == 0
|
assert _score_version(entry, version) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# -- progressive title truncation (long transcribed box titles) ---------
|
||||||
|
|
||||||
|
from bggpipe.normalize import normalize_title # noqa: E402
|
||||||
|
from bggpipe.resolve import _truncation_heads # noqa: E402
|
||||||
|
|
||||||
|
CIV_TITLE = (
|
||||||
|
"CIVILIZATION Game of the Heroic Age - The Dawn of History 8000 BC to 250 BC"
|
||||||
|
)
|
||||||
|
AH_HINT = "The Avalon Hill Game Company, Baltimore, Maryland"
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(title, **kw):
|
||||||
|
return TitleEntry(title_raw=title, title_normalized=normalize_title(title), **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def test_truncation_heads_order_and_dedupe():
|
||||||
|
heads = _truncation_heads(CIV_TITLE)
|
||||||
|
assert heads[0] == "CIVILIZATION Game of the Heroic Age" # before separator
|
||||||
|
assert heads[1] == "CIVILIZATION" # before the "Game ..." descriptor
|
||||||
|
assert len(heads) <= 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_truncation_heads_game_word_without_separator():
|
||||||
|
assert _truncation_heads("SORCERER The Game of Magical Conflict")[0] == "SORCERER"
|
||||||
|
|
||||||
|
|
||||||
|
def test_truncation_heads_two_word_fallback():
|
||||||
|
heads = _truncation_heads(
|
||||||
|
"STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century"
|
||||||
|
)
|
||||||
|
assert heads[-1] == "STARFORCE ALPHA"
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_titles_get_no_heads():
|
||||||
|
assert _truncation_heads("Catan") == []
|
||||||
|
assert _truncation_heads("Herbaceous") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_civilization_resolves_via_truncation(client):
|
||||||
|
entry = _entry(
|
||||||
|
CIV_TITLE,
|
||||||
|
publisher_hint=AH_HINT,
|
||||||
|
edition_hint="Bookcase Game",
|
||||||
|
language_hint="English",
|
||||||
|
)
|
||||||
|
row = resolve_entry(client, entry)
|
||||||
|
assert (row.match_status, row.bgg_id) == ("auto", 71)
|
||||||
|
# publisher cue picks the Avalon Hill version, not Hartland/Gibsons
|
||||||
|
assert row.version_status == "version_auto"
|
||||||
|
assert row.version_id == 71001
|
||||||
|
|
||||||
|
|
||||||
|
def test_advanced_civilization_resolves_as_expansion(client):
|
||||||
|
entry = _entry(
|
||||||
|
"ADVANCED CIVILIZATION Game Expansion of the Heroic Age - "
|
||||||
|
"Featuring New Civilization, Commodity, and Calamity Cards",
|
||||||
|
publisher_hint=AH_HINT,
|
||||||
|
edition_hint="Bookcase Game",
|
||||||
|
language_hint="English",
|
||||||
|
)
|
||||||
|
row = resolve_entry(client, entry)
|
||||||
|
assert (row.match_status, row.bgg_id, row.type) == (
|
||||||
|
"auto",
|
||||||
|
177,
|
||||||
|
"boardgameexpansion",
|
||||||
|
)
|
||||||
|
assert row.version_status == "version_auto"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sorcerer_publisher_tiebreak_beats_owned_dominance(client):
|
||||||
|
"""Head search finds TWO games named 'Sorcerer' (SPI 1975 vs White
|
||||||
|
Wizard 2019, which has far more owners) — the SPI publisher cue must
|
||||||
|
pick the SPI game, and the edition cue the Designer's Edition."""
|
||||||
|
entry = _entry(
|
||||||
|
"SORCERER The Game of Magical Conflict",
|
||||||
|
publisher_hint="Simulations Publications Incorporated (SPI)",
|
||||||
|
edition_hint="Designer's Edition",
|
||||||
|
language_hint="English",
|
||||||
|
)
|
||||||
|
row = resolve_entry(client, entry)
|
||||||
|
assert (row.match_status, row.bgg_id) == ("auto", 3585)
|
||||||
|
assert row.version_status == "version_auto"
|
||||||
|
assert row.version_name == "SPI Designer's Edition"
|
||||||
|
|
||||||
|
|
||||||
|
def test_starforce_two_word_head_matches_full_title(client):
|
||||||
|
entry = _entry(
|
||||||
|
"STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century",
|
||||||
|
publisher_hint="Simulations Publications Incorporated (SPI)",
|
||||||
|
edition_hint="Designer's Edition",
|
||||||
|
language_hint="English",
|
||||||
|
)
|
||||||
|
row = resolve_entry(client, entry)
|
||||||
|
assert (row.match_status, row.bgg_id) == ("auto", 2524)
|
||||||
|
assert row.version_status == "version_auto"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_year_hint_never_drives_a_version(client):
|
||||||
|
"""Flat Top's box says 1942 (the theme, not the print year): version
|
||||||
|
scoring must not pick any version off the back of it."""
|
||||||
|
entry = _entry("FLAT TOP", year_hint=1942, language_hint="English")
|
||||||
|
row = resolve_entry(client, entry)
|
||||||
|
assert (row.match_status, row.bgg_id) == ("auto", 2529)
|
||||||
|
assert row.version_status == "version_unknown"
|
||||||
|
assert row.version_id is None
|
||||||
|
|||||||
Reference in New Issue
Block a user