RPGs become local library citizens: identified, enriched, never uploaded

When the board-game search (and truncation heads) runs dry, resolve
falls back to type=rpgitem — the geekdo database is shared, so the same
API, token, cache, and classification machinery apply. Matched rpgitems
flow through review and enrich normally but diff routes them to a
local_only bucket, structurally outside to_add/to_update: their
collections live on RPGGeek, beyond this pipeline's write scope. The
library page gains an All/Board games/RPGs filter and an "RPG · local
only" badge; the catalog tags them too. Fixture generators write blanket
empty rpgitem stubs for every known query (the fallback fires for every
unmatched title) with real synthetic entries for Alice Is Missing.

Data: both Alice rows re-resolved from unmatched to auto rpgitem
matches. First diff since the audit reworks also lands their real-data
consequences: Dungeon! gains its TSR edition update on a versionless
copy the old claim ordering missed, to_add rows carry unioned reshoot
provenance, and the Herbaceous typo row's survivor is now the
correctly-spelled title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-02 19:01:09 -04:00
parent 33cbfffbd6
commit 74fc4fe847
66 changed files with 204 additions and 54 deletions
+1
View File
@@ -25,6 +25,7 @@ BGG" logo linking back to boardgamegeek.com.
## Endpoints (XML API2 — the only sanctioned read API)
- Search: `https://boardgamegeek.com/xmlapi2/search?query=<title>&type=boardgame,boardgameexpansion`
- RPG search: same endpoint with `type=rpgitem` — the geekdo database is shared across BGG/RPGGeek, so the same API and token serve RPG products. bggpipe uses this as a fallback for titles absent from the board-game types; matched rpgitems are LOCAL-ONLY (enriched, browsable, never uploaded — RPG collections live on rpggeek.com, outside this pipeline's write scope).
- Thing (details/stats): `https://boardgamegeek.com/xmlapi2/thing?id=<id1,id2,...>&stats=1` — accepts comma-separated IDs; batch (~20) to reduce request count.
- Thing versions: `https://boardgamegeek.com/xmlapi2/thing?id=<id>&versions=1` — lists every published edition/version of a game, each with its own version id, name, publisher, year, and language. Used to match photo edition cues to a concrete version.
- Collection: `https://boardgamegeek.com/xmlapi2/collection?username=<user>&own=1` — add `&version=1` to include version info on collection items.
+1
View File
@@ -47,6 +47,7 @@ Full design lives in `bgg-shelf-pipeline-spec.md` (read it before changing pipel
- Base game vs. expansion vs. new edition is the top failure mode — bias matching toward `ambiguous` over auto-match ("Wingspan Europe" must not match base Wingspan).
- Editions/versions matter: Eric owns multiple editions of some games — each is a separate collection entry (keyed by `collid` on BGG). Never guess a version: no legible cues → `version_unknown` and a version-less collection entry.
- Normalize titles (casefold, strip punctuation/articles, special chars like é/&/:) identically on both sides of a match; dedupe across photos but keep `source_photos` provenance.
- **RPGs are local-only citizens**: when the board-game search runs dry, resolve falls back to `type=rpgitem` (same geekdo API/token). Matched rpgitems enrich into the library but diff routes them to `local_only` — they must never reach `to_add.csv`/upload (their collection lives on RPGGeek, out of scope).
- Detailed BGG API behavior (202 queueing, collection-endpoint quirks, endpoints): use the `bgg-api` skill. **If the spec's BGG behavior changes, update the `bgg-api` skill to match** — they must not drift.
## Git
+4
View File
@@ -74,6 +74,10 @@ uv run bggpipe enrich # full metadata → data/games.json
Each stage skips work it has already done; `--force`/`--refresh` flags redo it. `review` without `--web` runs in the terminal instead. `upload` also has `--retry-failed`, `--verify` (re-fetches your collection and cross-checks the log), and runs a **headed** browser by default — BGG's Cloudflare check blocks headless ones, and a first login may need one human click before the session is saved locally and reused.
### RPGs on your shelves
Tabletop RPGs aren't in BGG's board-game database — they live on RPGGeek, which shares the same underlying API. When a title isn't found as a board game, bggpipe retries as an RPG: matches are identified, enriched, and browsable in the library (filter: RPGs), but they stay **local only** — they're never uploaded, since your BGG collection can't hold them.
### Taking good shelf photos
Straight-on, one shelf (or part of one) per shot, close enough that spine text is legible to a human. If you can't read it, the model can't either. Overlap between shots is fine: duplicate reads are deduped automatically, with the merge shown (and veto-able) in review. Boxes the model spots but can't identify become retake prompts in `unidentified.json` and the review UI's "reshoot" list: photograph those boxes up close, drop the new photo in `photos/`, and run `extract` again.
+36 -36
View File
@@ -1,36 +1,36 @@
title_raw,bgg_id,bgg_name,year,type,match_status,version_id,version_name,version_status,candidates_json,version_candidates_json,source_photos,merged_into
ROYALS,165986,Royals,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 165986, ""name"": ""Royals"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,
CAT CRIMES,235096,Cat Crimes,2017,boardgame,auto,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg,
COSMIC ENCOUNTER,40529,Cosmic Encounter,2008,boardgame,auto,,,version_unknown,"[{""bgg_id"": 39, ""name"": ""Cosmic Encounter"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 7000, ""rank"": 800}, {""bgg_id"": 40529, ""name"": ""Cosmic Encounter"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 75000, ""rank"": 150}]",[],IMG_4499.jpeg,
SHERIFF OF NOTTINGHAM,157969,Sheriff of Nottingham,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 157969, ""name"": ""Sheriff of Nottingham"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,
5 MINUTE DUNGEON,207830,5-Minute Dungeon,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 207830, ""name"": ""5-Minute Dungeon"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,
Britannia,240,Britannia,1986,boardgame,auto,25668,Avalon Hill English edition,version_auto,"[{""bgg_id"": 240, ""name"": ""Britannia"", ""year"": 1986, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 25668, ""name"": ""Avalon Hill English edition"", ""year"": 1986, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,
CIVILIZATION Game of the Heroic Age - The Dawn of History 8000 BC to 250 BC,71,Civilization,1980,boardgame,auto,71001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 71, ""name"": ""Civilization"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 31.2, ""owned"": null, ""rank"": null}]","[{""version_id"": 71001, ""name"": ""Avalon Hill English edition"", ""year"": 1981, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,
"ADVANCED CIVILIZATION Game Expansion of the Heroic Age - Featuring New Civilization, Commodity, and Calamity Cards",177,Advanced Civilization,1991,boardgameexpansion,auto,177001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 177, ""name"": ""Advanced Civilization"", ""year"": 1991, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 33.1, ""owned"": null, ""rank"": null}]","[{""version_id"": 177001, ""name"": ""Avalon Hill English edition"", ""year"": 1991, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,
DOCTOR WHO The Game of Time & Space,3090,Doctor Who: The Game of Time & Space,1980,boardgame,auto,309001,Games Workshop English edition,version_auto,"[{""bgg_id"": 3090, ""name"": ""Doctor Who: The Game of Time & Space"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 309001, ""name"": ""Games Workshop English edition"", ""year"": 1980, ""publishers"": [""Games Workshop Ltd.""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,
SORCERER The Game of Magical Conflict,3585,Sorcerer,1975,boardgame,auto,358501,SPI Designer's Edition,version_auto,"[{""bgg_id"": 3585, ""name"": ""Sorcerer"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 900, ""rank"": 5000}, {""bgg_id"": 244115, ""name"": ""Sorcerer"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 6000, ""rank"": 1500}]","[{""version_id"": 358501, ""name"": ""SPI Designer's Edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 358502, ""name"": ""SPI folio edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,
STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century,2524,StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century,1974,boardgame,auto,252401,SPI Designer's Edition,version_auto,"[{""bgg_id"": 2524, ""name"": ""StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century"", ""year"": 1974, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 252401, ""name"": ""SPI Designer's Edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 252402, ""name"": ""SPI folio edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,
SKIP-BO,1078,Skip-Bo,1967,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1078, ""name"": ""Skip-Bo"", ""year"": 1967, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,
a Gentle Rain,268163,A Gentle Rain,2019,boardgame,auto,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg,
PATCH WORK,163412,Patchwork,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 163412, ""name"": ""Patchwork"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 94.7, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,
SANTORINI,194655,Santorini,2016,boardgame,auto,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg,
Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,
Cards Against Humanity,50381,Cards Against Humanity,2009,boardgame,auto,,,version_unknown,"[{""bgg_id"": 50381, ""name"": ""Cards Against Humanity"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,
Joking Hazard,193621,Joking Hazard,2016,boardgame,auto,19362101,First edition,version_auto,"[{""bgg_id"": 193621, ""name"": ""Joking Hazard"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 19362101, ""name"": ""First edition"", ""year"": 2016, ""publishers"": [""Cyanide & Happiness""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4502.jpeg,
EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,
Hebarceos,195314,Herbaceous,2017,boardgame,merged,,,version_unknown,[],[],IMG_4502.jpeg,Herbaceous
Wiz-War,104710,Wiz-War,2012,boardgame,approved,,,version_unknown,"[{""bgg_id"": 1218, ""name"": ""Wiz-War"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 4000, ""rank"": 2500}, {""bgg_id"": 104710, ""name"": ""Wiz-War"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 8000, ""rank"": 1200}]",[],IMG_4502.jpeg,
SCRAWL,218866,Scrawl,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 218866, ""name"": ""Scrawl"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,
FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529, ""name"": ""Flat Top"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,
Diplomacy,483,Diplomacy,1959,boardgame,auto,,,version_unknown,"[{""bgg_id"": 483, ""name"": ""Diplomacy"", ""year"": 1959, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,
Age of Renaissance,26,Age of Renaissance,1996,boardgame,auto,2601,Avalon Hill English edition,version_auto,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 2601, ""name"": ""Avalon Hill English edition"", ""year"": 1996, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,
History of the World,224,History of the World,1991,boardgame,auto,22401,Avalon Hill English edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 22401, ""name"": ""Avalon Hill English edition"", ""year"": 1993, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,
DUNGEON!,1339,Dungeon!,1975,boardgame,auto,133903,TSR New Dungeon edition,version_approved,"[{""bgg_id"": 1339, ""name"": ""Dungeon!"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 133901, ""name"": ""TSR first edition"", ""year"": 1975, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133902, ""name"": ""TSR second edition"", ""year"": 1980, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133903, ""name"": ""TSR New Dungeon edition"", ""year"": 1989, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,
CAT CRIMES,235096,Cat Crimes,2017,boardgame,merged,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg;IMG_4505.jpeg,CAT CRIMES
a Gentle Rain,268163,A Gentle Rain,2019,boardgame,merged,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4507.jpeg,a Gentle Rain
SANTORINI,194655,Santorini,2016,boardgame,merged,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg,SANTORINI
Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,Utter Nonsense!
EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4505.jpeg,EXPLODING KITTENS
Herbaceous,195314,Herbaceous,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 195314, ""name"": ""Herbaceous"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4507.jpeg,
ALICE IS MISSING: SILENT FALLS EXPANSION,,,,,unmatched,,,,[],[],IMG_4566.jpeg,
ALICE IS MISSING: A SILENT ROLE PLAYING GAME,,,,,unmatched,,,,[],[],IMG_4566.jpeg,
title_raw,bgg_id,bgg_name,year,type,match_status,version_id,version_name,version_status,candidates_json,version_candidates_json,source_photos,merged_into,dedupe_veto
ROYALS,165986,Royals,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 165986, ""name"": ""Royals"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,,
CAT CRIMES,235096,Cat Crimes,2017,boardgame,auto,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg;IMG_4505.jpeg,,
COSMIC ENCOUNTER,40529,Cosmic Encounter,2008,boardgame,auto,,,version_unknown,"[{""bgg_id"": 39, ""name"": ""Cosmic Encounter"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 7000, ""rank"": 800}, {""bgg_id"": 40529, ""name"": ""Cosmic Encounter"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 75000, ""rank"": 150}]",[],IMG_4499.jpeg,,
SHERIFF OF NOTTINGHAM,157969,Sheriff of Nottingham,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 157969, ""name"": ""Sheriff of Nottingham"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,,
5 MINUTE DUNGEON,207830,5-Minute Dungeon,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 207830, ""name"": ""5-Minute Dungeon"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,,
Britannia,240,Britannia,1986,boardgame,auto,25668,Avalon Hill English edition,version_auto,"[{""bgg_id"": 240, ""name"": ""Britannia"", ""year"": 1986, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 25668, ""name"": ""Avalon Hill English edition"", ""year"": 1986, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,,
CIVILIZATION Game of the Heroic Age - The Dawn of History 8000 BC to 250 BC,71,Civilization,1980,boardgame,auto,71001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 71, ""name"": ""Civilization"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 31.2, ""owned"": null, ""rank"": null}]","[{""version_id"": 71001, ""name"": ""Avalon Hill English edition"", ""year"": 1981, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,,
"ADVANCED CIVILIZATION Game Expansion of the Heroic Age - Featuring New Civilization, Commodity, and Calamity Cards",177,Advanced Civilization,1991,boardgameexpansion,auto,177001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 177, ""name"": ""Advanced Civilization"", ""year"": 1991, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 33.1, ""owned"": null, ""rank"": null}]","[{""version_id"": 177001, ""name"": ""Avalon Hill English edition"", ""year"": 1991, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,,
DOCTOR WHO The Game of Time & Space,3090,Doctor Who: The Game of Time & Space,1980,boardgame,auto,309001,Games Workshop English edition,version_auto,"[{""bgg_id"": 3090, ""name"": ""Doctor Who: The Game of Time & Space"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 309001, ""name"": ""Games Workshop English edition"", ""year"": 1980, ""publishers"": [""Games Workshop Ltd.""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,,
SORCERER The Game of Magical Conflict,3585,Sorcerer,1975,boardgame,auto,358501,SPI Designer's Edition,version_auto,"[{""bgg_id"": 3585, ""name"": ""Sorcerer"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 900, ""rank"": 5000}, {""bgg_id"": 244115, ""name"": ""Sorcerer"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 6000, ""rank"": 1500}]","[{""version_id"": 358501, ""name"": ""SPI Designer's Edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 358502, ""name"": ""SPI folio edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,,
STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century,2524,StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century,1974,boardgame,auto,252401,SPI Designer's Edition,version_auto,"[{""bgg_id"": 2524, ""name"": ""StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century"", ""year"": 1974, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 252401, ""name"": ""SPI Designer's Edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 252402, ""name"": ""SPI folio edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,,
SKIP-BO,1078,Skip-Bo,1967,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1078, ""name"": ""Skip-Bo"", ""year"": 1967, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4542.jpeg,,
a Gentle Rain,268163,A Gentle Rain,2019,boardgame,auto,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4507.jpeg,,
PATCH WORK,163412,Patchwork,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 163412, ""name"": ""Patchwork"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 94.7, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,,
SANTORINI,194655,Santorini,2016,boardgame,auto,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg,,
Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,,
Cards Against Humanity,50381,Cards Against Humanity,2009,boardgame,auto,,,version_unknown,"[{""bgg_id"": 50381, ""name"": ""Cards Against Humanity"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,,
Joking Hazard,193621,Joking Hazard,2016,boardgame,auto,19362101,First edition,version_auto,"[{""bgg_id"": 193621, ""name"": ""Joking Hazard"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 19362101, ""name"": ""First edition"", ""year"": 2016, ""publishers"": [""Cyanide & Happiness""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4502.jpeg,,
EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4505.jpeg,,
Hebarceos,195314,Herbaceous,2017,boardgame,merged,,,version_unknown,[],[],IMG_4502.jpeg,Herbaceous,
Wiz-War,104710,Wiz-War,2012,boardgame,approved,,,version_unknown,"[{""bgg_id"": 1218, ""name"": ""Wiz-War"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 4000, ""rank"": 2500}, {""bgg_id"": 104710, ""name"": ""Wiz-War"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 8000, ""rank"": 1200}]",[],IMG_4502.jpeg;IMG_4504.jpeg;IMG_4528.jpeg,,
SCRAWL,218866,Scrawl,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 218866, ""name"": ""Scrawl"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,,
FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529, ""name"": ""Flat Top"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,,
Diplomacy,483,Diplomacy,1959,boardgame,auto,,,version_unknown,"[{""bgg_id"": 483, ""name"": ""Diplomacy"", ""year"": 1959, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,,
Age of Renaissance,26,Age of Renaissance,1996,boardgame,auto,2601,Avalon Hill English edition,version_auto,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 2601, ""name"": ""Avalon Hill English edition"", ""year"": 1996, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,,
History of the World,224,History of the World,1991,boardgame,auto,22401,Avalon Hill English edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 22401, ""name"": ""Avalon Hill English edition"", ""year"": 1993, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,,
DUNGEON!,1339,Dungeon!,1975,boardgame,auto,133903,TSR New Dungeon edition,version_approved,"[{""bgg_id"": 1339, ""name"": ""Dungeon!"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 133901, ""name"": ""TSR first edition"", ""year"": 1975, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133902, ""name"": ""TSR second edition"", ""year"": 1980, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133903, ""name"": ""TSR New Dungeon edition"", ""year"": 1989, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,,
CAT CRIMES,235096,Cat Crimes,2017,boardgame,merged,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg;IMG_4505.jpeg,CAT CRIMES,
a Gentle Rain,268163,A Gentle Rain,2019,boardgame,merged,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4507.jpeg,a Gentle Rain,
SANTORINI,194655,Santorini,2016,boardgame,merged,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg,SANTORINI,
Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,Utter Nonsense!,
EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4505.jpeg,EXPLODING KITTENS,
Herbaceous,195314,Herbaceous,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 195314, ""name"": ""Herbaceous"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4507.jpeg,,
ALICE IS MISSING: SILENT FALLS EXPANSION,400002,Alice Is Missing: Silent Falls Expansion,2023,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 400002, ""name"": ""Alice Is Missing: Silent Falls Expansion"", ""year"": 2023, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4566.jpeg,,
ALICE IS MISSING: A SILENT ROLE PLAYING GAME,400001,Alice Is Missing: A Silent Role Playing Game,2020,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 400001, ""name"": ""Alice Is Missing: A Silent Role Playing Game"", ""year"": 2020, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4566.jpeg,,
1 title_raw bgg_id bgg_name year type match_status version_id version_name version_status candidates_json version_candidates_json source_photos merged_into dedupe_veto
2 ROYALS 165986 Royals 2014 boardgame auto version_unknown [{"bgg_id": 165986, "name": "Royals", "year": 2014, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4499.jpeg
3 CAT CRIMES 235096 Cat Crimes 2017 boardgame auto 360982 English edition version_auto [{"bgg_id": 235096, "name": "Cat Crimes", "year": 2017, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 360982, "name": "English edition", "year": 2017, "publishers": ["ThinkFun"], "languages": ["English"], "score": 3}] IMG_4499.jpeg IMG_4499.jpeg;IMG_4505.jpeg
4 COSMIC ENCOUNTER 40529 Cosmic Encounter 2008 boardgame auto version_unknown [{"bgg_id": 39, "name": "Cosmic Encounter", "year": 1977, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 7000, "rank": 800}, {"bgg_id": 40529, "name": "Cosmic Encounter", "year": 2008, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 75000, "rank": 150}] [] IMG_4499.jpeg
5 SHERIFF OF NOTTINGHAM 157969 Sheriff of Nottingham 2014 boardgame auto version_unknown [{"bgg_id": 157969, "name": "Sheriff of Nottingham", "year": 2014, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4499.jpeg
6 5 MINUTE DUNGEON 207830 5-Minute Dungeon 2017 boardgame auto version_unknown [{"bgg_id": 207830, "name": "5-Minute Dungeon", "year": 2017, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4499.jpeg
7 Britannia 240 Britannia 1986 boardgame auto 25668 Avalon Hill English edition version_auto [{"bgg_id": 240, "name": "Britannia", "year": 1986, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 25668, "name": "Avalon Hill English edition", "year": 1986, "publishers": ["The Avalon Hill Game Co"], "languages": ["English"], "score": 3}] IMG_4500.jpeg
8 CIVILIZATION Game of the Heroic Age - The Dawn of History 8000 BC to 250 BC 71 Civilization 1980 boardgame auto 71001 Avalon Hill English edition version_auto [{"bgg_id": 71, "name": "Civilization", "year": 1980, "type": "boardgame", "exact": true, "fuzzy": 31.2, "owned": null, "rank": null}] [{"version_id": 71001, "name": "Avalon Hill English edition", "year": 1981, "publishers": ["The Avalon Hill Game Co"], "languages": ["English"], "score": 3}] IMG_4500.jpeg
9 ADVANCED CIVILIZATION Game Expansion of the Heroic Age - Featuring New Civilization, Commodity, and Calamity Cards 177 Advanced Civilization 1991 boardgameexpansion auto 177001 Avalon Hill English edition version_auto [{"bgg_id": 177, "name": "Advanced Civilization", "year": 1991, "type": "boardgameexpansion", "exact": true, "fuzzy": 33.1, "owned": null, "rank": null}] [{"version_id": 177001, "name": "Avalon Hill English edition", "year": 1991, "publishers": ["The Avalon Hill Game Co"], "languages": ["English"], "score": 3}] IMG_4500.jpeg
10 DOCTOR WHO The Game of Time & Space 3090 Doctor Who: The Game of Time & Space 1980 boardgame auto 309001 Games Workshop English edition version_auto [{"bgg_id": 3090, "name": "Doctor Who: The Game of Time & Space", "year": 1980, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 309001, "name": "Games Workshop English edition", "year": 1980, "publishers": ["Games Workshop Ltd."], "languages": ["English"], "score": 3}] IMG_4500.jpeg
11 SORCERER The Game of Magical Conflict 3585 Sorcerer 1975 boardgame auto 358501 SPI Designer's Edition version_auto [{"bgg_id": 3585, "name": "Sorcerer", "year": 1975, "type": "boardgame", "exact": true, "fuzzy": 39.0, "owned": 900, "rank": 5000}, {"bgg_id": 244115, "name": "Sorcerer", "year": 2019, "type": "boardgame", "exact": true, "fuzzy": 39.0, "owned": 6000, "rank": 1500}] [{"version_id": 358501, "name": "SPI Designer's Edition", "year": 1975, "publishers": ["Simulations Publications, Inc. (SPI)"], "languages": ["English"], "score": 5}, {"version_id": 358502, "name": "SPI folio edition", "year": 1975, "publishers": ["Simulations Publications, Inc. (SPI)"], "languages": ["English"], "score": 3}] IMG_4500.jpeg
12 STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century 2524 StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century 1974 boardgame auto 252401 SPI Designer's Edition version_auto [{"bgg_id": 2524, "name": "StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century", "year": 1974, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 252401, "name": "SPI Designer's Edition", "year": 1974, "publishers": ["Simulations Publications, Inc. (SPI)"], "languages": ["English"], "score": 5}, {"version_id": 252402, "name": "SPI folio edition", "year": 1974, "publishers": ["Simulations Publications, Inc. (SPI)"], "languages": ["English"], "score": 3}] IMG_4500.jpeg
13 SKIP-BO 1078 Skip-Bo 1967 boardgame auto version_unknown [{"bgg_id": 1078, "name": "Skip-Bo", "year": 1967, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg IMG_4501.jpeg;IMG_4542.jpeg
14 a Gentle Rain 268163 A Gentle Rain 2019 boardgame auto 268164 Bloom Edition version_auto [{"bgg_id": 268163, "name": "A Gentle Rain", "year": 2019, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 268164, "name": "Bloom Edition", "year": 2021, "publishers": ["Mondo Games"], "languages": ["English"], "score": 3}] IMG_4501.jpeg IMG_4501.jpeg;IMG_4507.jpeg
15 PATCH WORK 163412 Patchwork 2014 boardgame auto version_unknown [{"bgg_id": 163412, "name": "Patchwork", "year": 2014, "type": "boardgame", "exact": false, "fuzzy": 94.7, "owned": null, "rank": null}] [] IMG_4501.jpeg
16 SANTORINI 194655 Santorini 2016 boardgame auto version_unknown [{"bgg_id": 194655, "name": "Santorini", "year": 2016, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg;IMG_4502.jpeg IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg
17 Utter Nonsense! 181254 Utter Nonsense 2015 boardgame auto version_unknown [{"bgg_id": 181254, "name": "Utter Nonsense", "year": 2015, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg
18 Cards Against Humanity 50381 Cards Against Humanity 2009 boardgame auto version_unknown [{"bgg_id": 50381, "name": "Cards Against Humanity", "year": 2009, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg
19 Joking Hazard 193621 Joking Hazard 2016 boardgame auto 19362101 First edition version_auto [{"bgg_id": 193621, "name": "Joking Hazard", "year": 2016, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 19362101, "name": "First edition", "year": 2016, "publishers": ["Cyanide & Happiness"], "languages": ["English"], "score": 3}] IMG_4501.jpeg;IMG_4502.jpeg
20 EXPLODING KITTENS 172225 Exploding Kittens 2015 boardgame auto version_unknown [{"bgg_id": 172225, "name": "Exploding Kittens", "year": 2015, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg IMG_4501.jpeg;IMG_4505.jpeg
21 Hebarceos 195314 Herbaceous 2017 boardgame merged version_unknown [] [] IMG_4502.jpeg Herbaceous
22 Wiz-War 104710 Wiz-War 2012 boardgame approved version_unknown [{"bgg_id": 1218, "name": "Wiz-War", "year": 1983, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 4000, "rank": 2500}, {"bgg_id": 104710, "name": "Wiz-War", "year": 2012, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 8000, "rank": 1200}] [] IMG_4502.jpeg IMG_4502.jpeg;IMG_4504.jpeg;IMG_4528.jpeg
23 SCRAWL 218866 Scrawl 2017 boardgame auto version_unknown [{"bgg_id": 218866, "name": "Scrawl", "year": 2017, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4502.jpeg
24 FLAT TOP 2529 Flat Top 1977 boardgame auto version_unknown [{"bgg_id": 2529, "name": "Flat Top", "year": 1977, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4502.jpeg
25 Diplomacy 483 Diplomacy 1959 boardgame auto version_unknown [{"bgg_id": 483, "name": "Diplomacy", "year": 1959, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4502.jpeg
26 Age of Renaissance 26 Age of Renaissance 1996 boardgame auto 2601 Avalon Hill English edition version_auto [{"bgg_id": 26, "name": "Age of Renaissance", "year": 1996, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 2601, "name": "Avalon Hill English edition", "year": 1996, "publishers": ["The Avalon Hill Game Co"], "languages": ["English"], "score": 3}] IMG_4502.jpeg
27 History of the World 224 History of the World 1991 boardgame auto 22401 Avalon Hill English edition version_auto [{"bgg_id": 224, "name": "History of the World", "year": 1991, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 22401, "name": "Avalon Hill English edition", "year": 1993, "publishers": ["The Avalon Hill Game Co"], "languages": ["English"], "score": 3}] IMG_4502.jpeg
28 DUNGEON! 1339 Dungeon! 1975 boardgame auto 133903 TSR New Dungeon edition version_approved [{"bgg_id": 1339, "name": "Dungeon!", "year": 1975, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 133901, "name": "TSR first edition", "year": 1975, "publishers": ["TSR"], "languages": ["English"], "score": 3}, {"version_id": 133902, "name": "TSR second edition", "year": 1980, "publishers": ["TSR"], "languages": ["English"], "score": 3}, {"version_id": 133903, "name": "TSR New Dungeon edition", "year": 1989, "publishers": ["TSR"], "languages": ["English"], "score": 3}] IMG_4502.jpeg
29 CAT CRIMES 235096 Cat Crimes 2017 boardgame merged 360982 English edition version_auto [{"bgg_id": 235096, "name": "Cat Crimes", "year": 2017, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 360982, "name": "English edition", "year": 2017, "publishers": ["ThinkFun"], "languages": ["English"], "score": 3}] IMG_4499.jpeg;IMG_4505.jpeg CAT CRIMES
30 a Gentle Rain 268163 A Gentle Rain 2019 boardgame merged 268164 Bloom Edition version_auto [{"bgg_id": 268163, "name": "A Gentle Rain", "year": 2019, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 268164, "name": "Bloom Edition", "year": 2021, "publishers": ["Mondo Games"], "languages": ["English"], "score": 3}] IMG_4501.jpeg;IMG_4507.jpeg a Gentle Rain
31 SANTORINI 194655 Santorini 2016 boardgame merged version_unknown [{"bgg_id": 194655, "name": "Santorini", "year": 2016, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg SANTORINI
32 Utter Nonsense! 181254 Utter Nonsense 2015 boardgame merged version_unknown [{"bgg_id": 181254, "name": "Utter Nonsense", "year": 2015, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg Utter Nonsense!
33 EXPLODING KITTENS 172225 Exploding Kittens 2015 boardgame merged version_unknown [{"bgg_id": 172225, "name": "Exploding Kittens", "year": 2015, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4501.jpeg;IMG_4505.jpeg EXPLODING KITTENS
34 Herbaceous 195314 Herbaceous 2017 boardgame auto version_unknown [{"bgg_id": 195314, "name": "Herbaceous", "year": 2017, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4507.jpeg
35 ALICE IS MISSING: SILENT FALLS EXPANSION 400002 Alice Is Missing: Silent Falls Expansion 2023 rpgitem unmatched auto version_unknown [] [{"bgg_id": 400002, "name": "Alice Is Missing: Silent Falls Expansion", "year": 2023, "type": "rpgitem", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4566.jpeg
36 ALICE IS MISSING: A SILENT ROLE PLAYING GAME 400001 Alice Is Missing: A Silent Role Playing Game 2020 rpgitem unmatched auto version_unknown [] [{"bgg_id": 400001, "name": "Alice Is Missing: A Silent Role Playing Game", "year": 2020, "type": "rpgitem", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4566.jpeg
+9 -9
View File
@@ -1,9 +1,9 @@
bgg_id,bgg_name,year,type,version_id,version_name,title_raw,source_photos
235096,Cat Crimes,2017,boardgame,360982,English edition,CAT CRIMES,IMG_4499.jpeg
1078,Skip-Bo,1967,boardgame,,,SKIP-BO,IMG_4501.jpeg
268163,A Gentle Rain,2019,boardgame,268164,Bloom Edition,a Gentle Rain,IMG_4501.jpeg
163412,Patchwork,2014,boardgame,,,PATCH WORK,IMG_4501.jpeg
181254,Utter Nonsense,2015,boardgame,,,Utter Nonsense!,IMG_4501.jpeg
50381,Cards Against Humanity,2009,boardgame,,,Cards Against Humanity,IMG_4501.jpeg
195314,Herbaceous,2017,boardgame,,,Hebarceos,IMG_4502.jpeg
218866,Scrawl,2017,boardgame,,,SCRAWL,IMG_4502.jpeg
bgg_id,bgg_name,year,type,version_id,version_name,title_raw,source_photos,second_copy
235096,Cat Crimes,2017,boardgame,360982,English edition,CAT CRIMES,IMG_4499.jpeg;IMG_4505.jpeg,
268163,A Gentle Rain,2019,boardgame,268164,Bloom Edition,a Gentle Rain,IMG_4501.jpeg;IMG_4507.jpeg,
1078,Skip-Bo,1967,boardgame,,,SKIP-BO,IMG_4501.jpeg;IMG_4542.jpeg,
163412,Patchwork,2014,boardgame,,,PATCH WORK,IMG_4501.jpeg,
181254,Utter Nonsense,2015,boardgame,,,Utter Nonsense!,IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,
50381,Cards Against Humanity,2009,boardgame,,,Cards Against Humanity,IMG_4501.jpeg,
218866,Scrawl,2017,boardgame,,,SCRAWL,IMG_4502.jpeg,
195314,Herbaceous,2017,boardgame,,,Herbaceous,IMG_4502.jpeg;IMG_4507.jpeg,
1 bgg_id bgg_name year type version_id version_name title_raw source_photos second_copy
2 235096 Cat Crimes 2017 boardgame 360982 English edition CAT CRIMES IMG_4499.jpeg IMG_4499.jpeg;IMG_4505.jpeg
3 1078 268163 Skip-Bo A Gentle Rain 1967 2019 boardgame 268164 Bloom Edition SKIP-BO a Gentle Rain IMG_4501.jpeg IMG_4501.jpeg;IMG_4507.jpeg
4 268163 1078 A Gentle Rain Skip-Bo 2019 1967 boardgame 268164 Bloom Edition a Gentle Rain SKIP-BO IMG_4501.jpeg IMG_4501.jpeg;IMG_4542.jpeg
5 163412 Patchwork 2014 boardgame PATCH WORK IMG_4501.jpeg
6 181254 Utter Nonsense 2015 boardgame Utter Nonsense! IMG_4501.jpeg IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg
7 50381 Cards Against Humanity 2009 boardgame Cards Against Humanity IMG_4501.jpeg
8 195314 218866 Herbaceous Scrawl 2017 boardgame Hebarceos SCRAWL IMG_4502.jpeg
9 218866 195314 Scrawl Herbaceous 2017 boardgame SCRAWL Herbaceous IMG_4502.jpeg IMG_4502.jpeg;IMG_4507.jpeg
+1
View File
@@ -8,3 +8,4 @@ collid,bgg_id,bgg_name,version_id,version_name
53430387,193621,Joking Hazard,19362101,First edition
53429831,26,Age of Renaissance,2601,Avalon Hill English edition
53429816,224,History of the World,22401,Avalon Hill English edition
53430097,1339,Dungeon!,133903,TSR New Dungeon edition
1 collid bgg_id bgg_name version_id version_name
8 53430387 193621 Joking Hazard 19362101 First edition
9 53429831 26 Age of Renaissance 2601 Avalon Hill English edition
10 53429816 224 History of the World 22401 Avalon Hill English edition
11 53430097 1339 Dungeon! 133903 TSR New Dungeon edition
+17
View File
@@ -110,6 +110,19 @@ SEARCHES: dict[str, list[tuple]] = {
"ALICE IS": [],
}
# rpgitem-search results for titles that live on RPGGeek, not BGG. Ids are
# SYNTHETIC like everything else here; the marker guards them. Queries not
# listed get an empty rpgitem stub automatically.
RPG = "rpgitem"
RPG_SEARCHES: dict[str, list[tuple]] = {
"ALICE IS MISSING: A SILENT ROLE PLAYING GAME": [
(400001, "Alice Is Missing: A Silent Role Playing Game", 2020, RPG, "primary")
],
"ALICE IS MISSING: SILENT FALLS EXPANSION": [
(400002, "Alice Is Missing: Silent Falls Expansion", 2023, RPG, "primary")
],
}
# ids-param -> [(bgg_id, name, year, type, owned, rank, [publishers])]
THING_STATS: dict[str, list[tuple]] = {
"39,40529": [
@@ -144,6 +157,8 @@ THING_STATS: dict[str, list[tuple]] = {
# bgg_id -> [(version_id, name, year, [publishers], [languages])]
VERSIONS: dict[int, list[tuple]] = {
400001: [], # Alice Is Missing (rpgitem): no cataloged versions in stubs
400002: [],
235096: [(360982, "English edition", 2017, ["ThinkFun"], ["English"])],
240: [
(
@@ -344,6 +359,8 @@ def main() -> None:
for query, results in SEARCHES.items():
key = cache_key("search", {"query": query, "type": SEARCH_TYPES})
files[key] = search_xml(results)
rpg_key = cache_key("search", {"query": query, "type": "rpgitem"})
files[rpg_key] = search_xml(RPG_SEARCHES.get(query, []))
for ids, things in THING_STATS.items():
files[cache_key("thing", {"id": ids, "stats": "1"})] = stats_xml(things)
for bgg_id, versions in VERSIONS.items():
+4
View File
@@ -106,6 +106,10 @@ def main() -> None:
key = cache_key("search", {"query": query, "type": SEARCH_TYPES})
total = items.count("<item ")
(FIXTURE_CACHE / key).write_text(f'<items total="{total}">{items}</items>')
# resolve falls back to an rpgitem search whenever the board-game
# search runs dry: every known query needs an (empty) answer there
rpg_key = cache_key("search", {"query": query, "type": "rpgitem"})
(FIXTURE_CACHE / rpg_key).write_text('<items total="0"></items>')
for (ids, flavor), xml in THINGS.items():
key = cache_key("thing", {"id": ids, flavor: "1"})
(FIXTURE_CACHE / key).write_text(xml)
+10 -1
View File
@@ -62,6 +62,7 @@ class DiffResult:
to_add: list[dict] = field(default_factory=list)
to_update: list[dict] = field(default_factory=list)
already_owned: list[str] = field(default_factory=list) # title_raw
local_only: list[str] = field(default_factory=list) # RPGs etc: never uploaded
second_copies: list[str] = field(default_factory=list) # notes for adds
disagreements: list[str] = field(default_factory=list) # report-only
unseen: list[CollectionItem] = field(default_factory=list)
@@ -137,6 +138,11 @@ def compute_diff(rows: list[dict], collection: list[CollectionItem]) -> DiffResu
if status not in RECOGNIZED_MATCH_STATUSES or not row["bgg_id"]:
result.pending.append(row["title_raw"])
continue
if row["type"] == "rpgitem":
# a real, identified game — but it lives on RPGGeek, not in the
# BGG collection: a library citizen only, never queued
result.local_only.append(row["title_raw"])
continue
result.recognized += 1
recognized.append(row)
if by_object.get(int(row["bgg_id"])):
@@ -309,11 +315,14 @@ def run_diff(cfg: Config, *, client: BGGClient | None = None) -> DiffResult:
atomic_write_csv(cfg.to_update_path, TO_UPDATE_COLUMNS, result.to_update)
merged_note = f" · {result.merged} merged duplicate(s)" if result.merged else ""
local_note = (
f" · {len(result.local_only)} local-only (RPGs)" if result.local_only else ""
)
typer.echo(
f"\n{result.recognized} recognized · {len(result.already_owned)} already "
f"owned · {len(result.to_add)} to add · {len(result.to_update)} version "
f"update(s) · {len(result.pending)} pending review · "
f"{result.rejected} rejected{merged_note}"
f"{result.rejected} rejected{merged_note}{local_note}"
)
if result.second_copies:
typer.echo("\nSecond copies (verify these on the dry run before upload):")
+22 -2
View File
@@ -207,7 +207,11 @@ def _truncation_heads(title_raw: str) -> list[str]:
def _plausible_candidates(
client: BGGClient, entry: TitleEntry, query: str, head_normalized: str = ""
client: BGGClient,
entry: TitleEntry,
query: str,
head_normalized: str = "",
types: str | None = None,
) -> 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
@@ -218,7 +222,8 @@ def _plausible_candidates(
if not entry.title_normalized:
return []
by_id: dict[int, Candidate] = {}
for result in client.search(query):
results = client.search(query, types) if types else client.search(query)
for result in results:
norm = normalize_title(result.name)
exact = norm == entry.title_normalized
fuzzy = fuzz.token_sort_ratio(norm, entry.title_normalized)
@@ -396,6 +401,21 @@ def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow:
cands = _plausible_candidates(client, entry, head, normalize_title(head))
if cands:
break
if not cands:
# Not a board game? RPGs live in the same geekdo database under
# type=rpgitem (same API, same token). A hit becomes a LOCAL
# library citizen: identified and enriched, never uploaded (diff
# routes rpgitem rows to local_only).
cands = _plausible_candidates(
client, entry, entry.title_raw, types="rpgitem"
)
if not cands:
for head in _truncation_heads(entry.title_raw):
cands = _plausible_candidates(
client, entry, head, normalize_title(head), types="rpgitem"
)
if cands:
break
row = _classify(client, entry, cands)
if row.match_status == "auto":
resolve_version(client, entry, row)
+3
View File
@@ -428,6 +428,9 @@ button.danger { color: var(--stop-ink); border-color: var(--stop); background: #
padding: .15rem .5rem; border-radius: 6px;
}
.filterbar { display: flex; gap: .6rem; margin-bottom: 1rem; flex-wrap: wrap; }
.filterbar button[aria-pressed="true"] {
background: var(--accent); border-color: var(--ink); color: #fff;
}
.filterbar input[type=search] {
font: inherit; padding: .35rem .7rem; min-width: 16rem;
border: var(--line); border-radius: var(--radius); background: #fff;
+2 -1
View File
@@ -21,7 +21,8 @@ function render() {
<td class="t">${esc(c.title_raw)}</td>
<td>${statusChip(c)}</td>
<td class="meta">${c.bgg_name ? esc(c.bgg_name) + (c.bgg_id ? " · " + esc(c.bgg_id) : "") : ""}
${c.version_name ? " · " + esc(c.version_name) : ""}</td>
${c.version_name ? " · " + esc(c.version_name) : ""}
${c.type === "rpgitem" ? ` <span class="chip open">RPG · local only</span>` : ""}</td>
<td class="meta">${c.photos.map(p =>
`<a href="/photos/view/${encodeURIComponent(p)}">${esc(p)}</a>`
).join(", ")}</td>
+22 -5
View File
@@ -1,12 +1,16 @@
<h1>Library</h1>
<div class="pagebar"><span id="libcount"></span></div>
<div class="filterbar">
<div class="filterbar" role="group" aria-label="filter by kind">
<input type="search" id="libsearch" placeholder="search your games…" aria-label="search library">
<button type="button" data-kind="" aria-pressed="true">All</button>
<button type="button" data-kind="boardgame" aria-pressed="false">Board games</button>
<button type="button" data-kind="rpgitem" aria-pressed="false">RPGs</button>
</div>
<div id="libbody"></div>
<script>
"use strict";
let GAMES = [];
let KIND = ""; // "" = all; "boardgame" also covers expansions
function gameCard(g) {
const art = g.thumbnail || g.image;
@@ -22,18 +26,25 @@ function gameCard(g) {
? `<img src="${esc(art)}" alt="" loading="lazy">`
: `<div class="noart" aria-hidden="true">${esc((g.name || "?").charAt(0).toUpperCase())}</div>`}
<div class="info">
<div class="gname">${esc(g.name)}${g.year ? ` <span class="meta">(${esc(g.year)})</span>` : ""}</div>
<div class="gname">${esc(g.name)}${g.year ? ` <span class="meta">(${esc(g.year)})</span>` : ""}
${g.type === "rpgitem" ? `<span class="chip open">RPG · local only</span>` : ""}</div>
<div class="gmeta">${[players, time, weight, rank].filter(Boolean).map(esc).join(" · ")}</div>
${g.version ? `<div class="gmeta">${esc(g.version.name || "")}</div>` : ""}
</div>
</article>`;
}
function matchesKind(g) {
if (!KIND) return true;
if (KIND === "boardgame") return g.type !== "rpgitem";
return g.type === "rpgitem";
}
function render() {
const q = document.getElementById("libsearch").value.trim().toLowerCase();
const rows = q
? GAMES.filter(g => (g.name + " " + (g.designers || []).join(" ")).toLowerCase().includes(q))
: GAMES;
let rows = GAMES.filter(matchesKind);
if (q) rows = rows.filter(g =>
(g.name + " " + (g.designers || []).join(" ")).toLowerCase().includes(q));
document.getElementById("libcount").innerHTML =
`<b>${rows.length}</b> of <b>${GAMES.length}</b> game(s)`;
document.getElementById("libbody").innerHTML = rows.length
@@ -56,6 +67,12 @@ async function refresh() {
}
document.getElementById("libsearch").addEventListener("input", render);
document.querySelectorAll("[data-kind]").forEach(b => b.addEventListener("click", () => {
KIND = b.dataset.kind;
document.querySelectorAll("[data-kind]").forEach(o =>
o.setAttribute("aria-pressed", String(o === b)));
render();
}));
refresh().catch(err => errorBanner(err.message || err));
pollLoop(refresh, 10000, () => showBanner(""));
</script>
+1
View File
@@ -367,6 +367,7 @@ def create_app(
"confidence": entry.confidence,
"photos": list(entry.source_photos),
"status": row["match_status"] if row else "awaiting_resolve",
"type": row["type"] if row else "",
"bgg_id": row["bgg_id"] if row else "",
"bgg_name": row["bgg_name"] if row else "",
"version_name": row["version_name"] if row else "",
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="1"><item type="rpgitem" id="400001"><name type="primary" value="Alice Is Missing: A Silent Role Playing Game"/><yearpublished value="2020"/></item></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="1"><item type="rpgitem" id="400002"><name type="primary" value="Alice Is Missing: Silent Falls Expansion"/><yearpublished value="2023"/></item></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items total="0"></items>
@@ -0,0 +1 @@
<items><item type="boardgame" id="400001"><name type="primary" value="game"/><versions></versions></item></items>
@@ -0,0 +1 @@
<items><item type="boardgame" id="400002"><name type="primary" value="game"/><versions></versions></item></items>
+8
View File
@@ -397,3 +397,11 @@ def test_second_copy_flag_travels_on_exhausted_adds():
result = compute_diff(rows, [_item(13, 900, version_id=123)])
(added,) = result.to_add
assert added["second_copy"] == "1"
def test_rpgitem_rows_are_local_only_never_queued():
rpg = {**_match("ALICE IS MISSING", "400001"), "type": "rpgitem"}
result = compute_diff([rpg, _match("Catan", "13")], [])
assert result.local_only == ["ALICE IS MISSING"]
assert [r["bgg_id"] for r in result.to_add] == ["13"] # RPG never queued
assert result.recognized == 1 # RPGs aren't counted against the queue
+13
View File
@@ -672,3 +672,16 @@ def test_merged_into_chains_resolve_to_terminal_survivor():
dedupe_matches([x, y, w], [])
assert y["match_status"] == "merged" and y["merged_into"] == "Wingspan"
assert x["merged_into"] == "Wingspan" # chain collapsed
def test_rpg_falls_back_to_rpgitem_search(client):
# Alice Is Missing: absent from the board-game search, present as an
# rpgitem — same geekdo API, resolved as a local library citizen
entry = TitleEntry(
title_raw="ALICE IS MISSING: A SILENT ROLE PLAYING GAME",
title_normalized="alice is missing silent role playing game",
)
row = resolve_entry(client, entry)
assert row.match_status == "auto"
assert row.type == "rpgitem"
assert row.bgg_id == 400001