diff --git a/data/matches.csv b/data/matches.csv index ea60167..74b2e01 100644 --- a/data/matches.csv +++ b/data/matches.csv @@ -29,7 +29,6 @@ TICKET TO RIDE,9209,Ticket to Ride,2004,boardgame,auto,,,version_ambiguous,"[{"" CATAN,13,Catan,1995,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 13, ""name"": ""Catan"", ""year"": 1995, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 243104, ""rank"": 626}, {""bgg_id"": 66056, ""name"": ""Catan Двубоят"", ""year"": 2010, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 21829, ""rank"": 1187}]","[{""version_id"": 347121, ""name"": ""English edition 2016"", ""year"": 2016, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 495726, ""name"": ""English edition 2019"", ""year"": 2019, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 568647, ""name"": ""English edition 2020"", ""year"": 2020, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 733924, ""name"": ""English edition 2023"", ""year"": 2023, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 416798, ""name"": ""Afrikaans edition"", ""year"": 0, ""publishers"": [""Catan Studio""], ""languages"": [""Afrikaans""], ""score"": 2}]",IMG_4504.jpeg;IMG_4542.jpeg,, RAMPAGE,97903,Rampage,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 97903, ""name"": ""Rampage"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4504.jpeg,, Onitama,160477,Onitama,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 160477, ""name"": ""Onitama"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4505.jpeg,, -MANTIS FALLS (a game of trust),,,,,unmatched,,,,[],[],IMG_4505.jpeg,, 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,, LABYRINTH,1219,Labyrinth,1986,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 1219, ""name"": ""Labyrinth"", ""year"": 1986, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 35117, ""rank"": 2116}, {""bgg_id"": 6546, ""name"": ""Labyrinth"", ""year"": 1940, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1078, ""rank"": 31020}, {""bgg_id"": 23074, ""name"": ""Labyrinth"", ""year"": 2000, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 63, ""rank"": null}, {""bgg_id"": 24422, ""name"": ""Labyrinth"", ""year"": 2001, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 121, ""rank"": null}, {""bgg_id"": 65696, ""name"": ""Labyrinth"", ""year"": null, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 46, ""rank"": null}]","[{""version_id"": 488621, ""name"": ""English edition 2017"", ""year"": 2017, ""publishers"": [""Ravensburger AG""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 591493, ""name"": ""English/German edition"", ""year"": 2018, ""publishers"": [""Ravensburger AG""], ""languages"": [""English"", ""German""], ""score"": 3}, {""version_id"": 361899, ""name"": ""Multilingual 30th anniversary edition"", ""year"": 2016, ""publishers"": [""Ravensburger AG""], ""languages"": [""English"", ""French"", ""German"", ""Italian""], ""score"": 3}, {""version_id"": 338520, ""name"": ""Multilingual edition"", ""year"": 2017, ""publishers"": [""Ravensburger AG""], ""languages"": [""English"", ""French"", ""German"", ""Italian"", ""Portuguese"", ""Spanish""], ""score"": 3}, {""version_id"": 440013, ""name"": ""Multilingual edition"", ""year"": 2019, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian""], ""score"": 3}, {""version_id"": 487001, ""name"": ""Multilingual edition"", ""year"": 2019, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian"", ""Spanish""], ""score"": 3}, {""version_id"": 571233, ""name"": ""Multilingual edition"", ""year"": 2021, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian"", ""Portuguese"", ""Spanish""], ""score"": 3}, {""version_id"": 630855, ""name"": ""Multilingual edition"", ""year"": 2022, ""publishers"": [""Ravensburger AG""], ""languages"": [""Dutch"", ""English"", ""French"", ""German"", ""Italian"", ""Portuguese"", ""Spanish""], ""score"": 3}]",IMG_4507.jpeg;IMG_4508.jpeg,, Super Mario Checkers Collector's Edition,,,,,unmatched,,,,[],[],IMG_4508.jpeg,, @@ -49,14 +48,12 @@ ROOK,1260,Rook,1906,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1260, ""name Kensington,2197,Kensington,1979,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2197, ""name"": ""Kensington"", ""year"": 1979, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1207, ""rank"": 29937}, {""bgg_id"": 276459, ""name"": ""Kensington"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 99, ""rank"": 20091}]",[],IMG_4512.jpeg,, Guillotine,116,Guillotine,1998,boardgame,auto,,,version_unknown,"[{""bgg_id"": 116, ""name"": ""Guillotine"", ""year"": 1998, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 34783, ""rank"": 1630}, {""bgg_id"": 11629, ""name"": ""Guillotine"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 87, ""rank"": 18073}, {""bgg_id"": 27373, ""name"": ""Guillotine"", ""year"": 1987, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 163, ""rank"": 19576}]",[],IMG_4512.jpeg,, THE DOOM THAT CAME TO ATLANTIC CITY,124548,The Doom That Came to Atlantic City,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 124548, ""name"": ""The Doom That Came to Atlantic City"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4513.jpeg,, -SMALLWORLD,,,,,unmatched,,,,[],[],IMG_4513.jpeg,, Flick'em UP!,169124,Flick 'em Up!,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 169124, ""name"": ""Flick 'em Up!"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4513.jpeg,, Castle Panic,43443,Castle Panic,2009,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 43443, ""name"": ""Castle Panic"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 468123, ""name"": ""English edition, eighteenth printing"", ""year"": 2018, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 222131, ""name"": ""English edition, eighth printing"", ""year"": 2013, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 265959, ""name"": ""English edition, eleventh printing"", ""year"": 2014, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 343185, ""name"": ""English edition, fifteenth printing"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 169933, ""name"": ""English edition, fifth printing"", ""year"": 2012, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 24624, ""name"": ""English edition, first printing"", ""year"": 2009, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 337551, ""name"": ""English edition, fourteenth printing"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 103068, ""name"": ""English edition, fourth printing"", ""year"": 2010, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4513.jpeg,, The Red Dragon Inn,24310,The Red Dragon Inn,2007,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 24310, ""name"": ""The Red Dragon Inn"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 16870, ""rank"": 2164}, {""bgg_id"": 33451, ""name"": ""The Red Dragon Inn 2"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 93.3, ""owned"": 8355, ""rank"": 2056}, {""bgg_id"": 66510, ""name"": ""The Red Dragon Inn 3"", ""year"": 2011, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 93.3, ""owned"": 6246, ""rank"": 2363}, {""bgg_id"": 142402, ""name"": ""The Red Dragon Inn 4"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 93.3, ""owned"": 4107, ""rank"": 3367}, {""bgg_id"": 455834, ""name"": ""The Red Dragon Inn 10"", ""year"": 2026, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 90.3, ""owned"": 162, ""rank"": null}]","[{""version_id"": 400341, ""name"": ""English eighth edition"", ""year"": 2017, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 620786, ""name"": ""English eleventh edition"", ""year"": 2021, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 762888, ""name"": ""English fifteenth edition"", ""year"": 2024, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 374258, ""name"": ""English fifth edition"", ""year"": 2012, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 33932, ""name"": ""English first edition"", ""year"": 2007, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 728027, ""name"": ""English fourteenth edition"", ""year"": 2023, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 312374, ""name"": ""English seventh edition"", ""year"": 2015, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 389768, ""name"": ""English sixth edition"", ""year"": 2014, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4514.jpeg,, Dixit,39856,Dixit,2008,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 39856, ""name"": ""Dixit"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 482308, ""name"": ""Afrikaans/English edition"", ""year"": 2019, ""publishers"": [""Libellud""], ""languages"": [""Afrikaans"", ""English""], ""score"": 3}, {""version_id"": 488774, ""name"": ""Arabic/English edition"", ""year"": 2019, ""publishers"": [""Boardgame Space"", ""Libellud""], ""languages"": [""Arabic"", ""English""], ""score"": 3}, {""version_id"": 296529, ""name"": ""Asmodee/Libellud English edition 2015"", ""year"": 2015, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 160064, ""name"": ""English edition 2011"", ""year"": 2011, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 193872, ""name"": ""English edition 2012"", ""year"": 2012, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 241272, ""name"": ""English edition 2013"", ""year"": 2013, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 256721, ""name"": ""English edition 2014"", ""year"": 2014, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 273210, ""name"": ""English edition 2014"", ""year"": 2014, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}]",IMG_4514.jpeg,, Huggermugger,2387,Huggermugger,1989,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2387, ""name"": ""Huggermugger"", ""year"": 1989, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4514.jpeg,, The Rocky Horror Trivia Game,20866,The Rocky Horror Trivia Game,2005,boardgame,auto,,,version_unknown,"[{""bgg_id"": 20866, ""name"": ""The Rocky Horror Trivia Game"", ""year"": 2005, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4514.jpeg,, -ART NOUVEAU ARTISTRY,,,,,unmatched,,,,[],[],IMG_4515.jpeg,, SPEECHLESS,196105,Speechless,2016,boardgame,approved,,,version_unknown,"[{""bgg_id"": 14370, ""name"": ""Speechless"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 94, ""rank"": null}, {""bgg_id"": 196105, ""name"": ""Speechless"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 493, ""rank"": 10370}]",[],IMG_4516.jpeg,, CATAN JUNIOR,125921,Catan: Junior,2012,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 184842, ""name"": ""Catan Junior"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 3961, ""rank"": 4440}, {""bgg_id"": 5824, ""name"": ""Catan Junior"", ""year"": 2003, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1225, ""rank"": 30649}, {""bgg_id"": 125921, ""name"": ""Catan: Junior"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 9917, ""rank"": 2616}]","[{""version_id"": 791740, ""name"": ""English edition 2025"", ""year"": 2025, ""publishers"": [""Catan GmbH"", ""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 558911, ""name"": ""English fourth edition"", ""year"": 2018, ""publishers"": [""Catan GmbH"", ""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 414618, ""name"": ""English second edition"", ""year"": 2016, ""publishers"": [""Catan GmbH"", ""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 476263, ""name"": ""English third edition"", ""year"": 2016, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}]",IMG_4516.jpeg,, SKYLANDERS PORTAL MASTER,156994,Skylanders Portal Master,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 156994, ""name"": ""Skylanders Portal Master"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4516.jpeg,, @@ -66,7 +63,6 @@ Pie Face!,18755,Pie Face!,1964,boardgame,approved,,,version_unknown,"[{""bgg_id" Walk the Plank!,35505,Walk the Plank!,2013,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 21834, ""name"": ""Walk the Plank"", ""year"": 2002, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 18, ""rank"": null}, {""bgg_id"": 29010, ""name"": ""Walk the Plank"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 179, ""rank"": 28080}, {""bgg_id"": 152950, ""name"": ""Walk the Plank"", ""year"": null, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 5, ""rank"": null}, {""bgg_id"": 35505, ""name"": ""Walk the Plank!"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 3127, ""rank"": 3700}]","[{""version_id"": 781669, ""name"": ""English MDG-4311 edition"", ""year"": 2013, ""publishers"": [""Mayday Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 189968, ""name"": ""English MYY4311 edition"", ""year"": 2013, ""publishers"": [""Mayday Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 483159, ""name"": ""Russian edition"", ""year"": 2013, ""publishers"": [""Cosmodrome Games"", ""Mayday Games""], ""languages"": [""Russian""], ""score"": 2}]",IMG_4517.jpeg;IMG_4549.jpeg,, AGRICOLA,31260,Agricola,2007,boardgame,auto,297589,Z-Man Games English fifth edition,version_auto,"[{""bgg_id"": 31260, ""name"": ""Agricola"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 297589, ""name"": ""Z-Man Games English fifth edition"", ""year"": 2013, ""publishers"": [""Filosofia Éditions"", ""Lookout Games"", ""Z-Man Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 165308, ""name"": ""999 Games Dutch second edition 2011"", ""year"": 2011, ""publishers"": [""999 Games"", ""Lookout Games""], ""languages"": [""Dutch""], ""score"": 2}, {""version_id"": 266028, ""name"": ""Hobby World Russian first edition"", ""year"": 2013, ""publishers"": [""Hobby World"", ""Lookout Games""], ""languages"": [""Russian""], ""score"": 2}, {""version_id"": 22673, ""name"": ""Lookout Games German first edition"", ""year"": 2007, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 64715, ""name"": ""Lookout Games German second edition 2008 with animeeples"", ""year"": 2008, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 219203, ""name"": ""Lookout Games German seventh edition 2012 with DSP and SdJ"", ""year"": 2012, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 64718, ""name"": ""Lookout Games German third edition 2009 with DSP and SdJ"", ""year"": 2009, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 113858, ""name"": ""Lookout Games Russian first edition 2011"", ""year"": 2011, ""publishers"": [""Lookout Games"", ""Smart Ltd""], ""languages"": [""Russian""], ""score"": 2}]",IMG_4518.jpeg,, METEOR,166109,Meteor,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 25172, ""name"": ""Meteor"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 56, ""rank"": null}, {""bgg_id"": 166109, ""name"": ""Meteor"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1396, ""rank"": 10371}, {""bgg_id"": 246740, ""name"": ""Meteors"", ""year"": 2018, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 92.3, ""owned"": 71, ""rank"": 25541}]",[],IMG_4518.jpeg,, -MAGE WARS ACADEMY CORE SET,,,,,unmatched,,,,[],[],IMG_4518.jpeg,, DOCTOR WHO THE card GAME,125675,Doctor Who: The Card Game,2012,boardgame,auto,,,version_unknown,"[{""bgg_id"": 125675, ""name"": ""Doctor Who: The Card Game"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4518.jpeg,, DUNGEONS & DRAGONS,140509,Dungeons & Dragons,1985,boardgame,auto,,,version_unknown,"[{""bgg_id"": 140509, ""name"": ""Dungeons & Dragons"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4519.jpeg,, DUNGEONS & DRAGONS FANTASY GAME RULES,,,,,unmatched,,,,[],[],IMG_4519.jpeg,, @@ -110,7 +106,7 @@ Greed Quest,9666,Greed Quest,2004,boardgame,auto,215137,English edition,version_ GLOOMHAVEN - Jaws of the Lion,291457,Gloomhaven: Jaws of the Lion,2020,boardgame,auto,,,version_unknown,"[{""bgg_id"": 291457, ""name"": ""Gloomhaven: Jaws of the Lion"", ""year"": 2020, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4538.jpeg,, SCRABBLE SLAM!,40508,Scrabble Slam!,2008,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 40508, ""name"": ""Scrabble Slam!"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 39335, ""name"": ""English edition"", ""year"": 2009, ""publishers"": [""Hasbro""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 644326, ""name"": ""English edition with Hasbro logo"", ""year"": 2008, ""publishers"": [""Hasbro"", ""Parker Brothers""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 25061, ""name"": ""English edition with Parker Brothers logo"", ""year"": 2008, ""publishers"": [""Hasbro"", ""Parker Brothers""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 294332, ""name"": ""English/French edition"", ""year"": 2008, ""publishers"": [""Hasbro"", ""Parker Brothers""], ""languages"": [""English"", ""French""], ""score"": 3}, {""version_id"": 480950, ""name"": ""English/French edition"", ""year"": 2014, ""publishers"": [""Hasbro""], ""languages"": [""English"", ""French""], ""score"": 3}, {""version_id"": 116530, ""name"": ""Dutch edition"", ""year"": 2009, ""publishers"": [""Hasbro""], ""languages"": [""Dutch""], ""score"": 2}, {""version_id"": 345823, ""name"": ""French edition"", ""year"": 2009, ""publishers"": [""Hasbro""], ""languages"": [""French""], ""score"": 2}, {""version_id"": 111167, ""name"": ""German edition"", ""year"": 2010, ""publishers"": [""Hasbro""], ""languages"": [""German""], ""score"": 2}]",IMG_4543.jpeg,, CAT STAX,,,,,unmatched,,,,[],[],IMG_4544.jpeg,, -POCKET FARKEL,,,,,unmatched,,,,[],[],IMG_4545.jpeg,, +POCKET FARKEL,3181,Farkle,1930,boardgame,approved,,,version_ambiguous,[],"[{""version_id"": 374844, ""name"": ""Legendary Games black flat pack edition"", ""year"": 1996, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 390425, ""name"": ""Legendary Games blister pack editon"", ""year"": 0, ""publishers"": [""Cardinal"", ""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 87931, ""name"": ""Legendary Games cannister edition"", ""year"": 0, ""publishers"": [""Legendary Games, Inc."", ""Reveal Entertainment, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 295385, ""name"": ""Legendary Games dice rolling tray edition"", ""year"": 2006, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 658688, ""name"": ""Legendary Games English/Spanish \""Alaska: The Last Frontier\"" edition"", ""year"": 1999, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English"", ""Spanish""], ""score"": 3}, {""version_id"": 374847, ""name"": ""Legendary Games flat pack edition"", ""year"": 0, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 247184, ""name"": ""Legendary Games flat pack edition 1996"", ""year"": 1996, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 374842, ""name"": ""Legendary Games flat pack edition 1996"", ""year"": 1996, ""publishers"": [""Legendary Games, Inc.""], ""languages"": [""English""], ""score"": 3}]",IMG_4545.jpeg,, Go Fish Yourself,173057,Go Fish Yourself,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 173057, ""name"": ""Go Fish Yourself"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4546.jpeg,, Hold your Breath!,150925,Hold Your Breath!,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 150925, ""name"": ""Hold Your Breath!"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4547.jpeg,, Dastardly Dirigibles,193485,Dastardly Dirigibles,2016,boardgame,auto,300446,English edition,version_auto,"[{""bgg_id"": 193485, ""name"": ""Dastardly Dirigibles"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 300446, ""name"": ""English edition"", ""year"": 2016, ""publishers"": [""Fireside Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4548.jpeg,, @@ -131,3 +127,4 @@ The Simpsons Trivia Game,2980,The Simpsons Trivia Game,2000,boardgame,auto,,,ver THE CAT GAME,231302,The Cat Game,2017,boardgame,auto,,,version_ambiguous,"[{""bgg_id"": 231302, ""name"": ""The Cat Game"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 361880, ""name"": ""English/French edition"", ""year"": 2017, ""publishers"": [""Spin Master Ltd.""], ""languages"": [""English"", ""French""], ""score"": 2}, {""version_id"": 433263, ""name"": ""German edition"", ""year"": 0, ""publishers"": [""Spin Master Ltd.""], ""languages"": [""German""], ""score"": 2}]",shelf-20260803-213756.jpg,, BLINK,1197,Blink,1995,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1197, ""name"": ""Blink"", ""year"": 1995, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 6143, ""rank"": 6149}, {""bgg_id"": 185341, ""name"": ""Blink!"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 111, ""rank"": null}, {""bgg_id"": 15852, ""name"": ""Blinki"", ""year"": 1910, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 90.9, ""owned"": 106, ""rank"": 30046}]",[],shelf-20260803-215650.jpg,, The Office Trivia Game,352819,The Office Trivia Game,2019,boardgame,approved,,,version_error,"[{""bgg_id"": 352819, ""name"": ""The Office Trivia Game"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 159, ""rank"": null}, {""bgg_id"": 36895, ""name"": ""The Office Trivia Game"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 432, ""rank"": 30024}]",[],shelf-20260803-215703.jpg,, +Patchwork,163412,Patchwork,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 163412, ""name"": ""Patchwork"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 124114, ""rank"": 148}, {""bgg_id"": 41585, ""name"": ""Patchwork"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 473, ""rank"": 14984}]",[],IMG_4501.jpeg,, diff --git a/data/title_edits.json b/data/title_edits.json index 655fdc2..17b84ad 100644 --- a/data/title_edits.json +++ b/data/title_edits.json @@ -57,5 +57,25 @@ "match": "Patch Work", "title_raw": "Patchwork", "confidence": "high" + }, + { + "match": "SMALLWORLD", + "title_raw": "SMALL WORLD", + "confidence": "high" + }, + { + "match": "ART NOUVEAU ARTISTRY", + "title_raw": "ARTISTRY", + "confidence": "high" + }, + { + "match": "MAGE WARS ACADEMY CORE SET", + "title_raw": "MAGE WARS ACADEMY", + "confidence": "high" + }, + { + "match": "MANTIS FALLS (a game of trust)", + "title_raw": "MANTIS FALLS", + "confidence": "high" } ] diff --git a/data/titles.json b/data/titles.json index f03e743..0769e04 100644 --- a/data/titles.json +++ b/data/titles.json @@ -417,7 +417,7 @@ "title_normalized": "onitama" }, { - "title_raw": "MANTIS FALLS (a game of trust)", + "title_raw": "MANTIS FALLS", "confidence": "high", "publisher_hint": "Diligent Games", "edition_hint": "", @@ -427,7 +427,7 @@ "source_photos": [ "IMG_4505.jpeg" ], - "title_normalized": "mantis falls game of trust" + "title_normalized": "mantis falls" }, { "title_raw": "Herbaceous", @@ -680,7 +680,7 @@ "title_normalized": "doom that came to atlantic city" }, { - "title_raw": "SMALLWORLD", + "title_raw": "SMALL WORLD", "confidence": "high", "publisher_hint": "Days of Wonder", "edition_hint": "", @@ -690,7 +690,7 @@ "source_photos": [ "IMG_4513.jpeg" ], - "title_normalized": "smallworld" + "title_normalized": "small world" }, { "title_raw": "Flick'em UP!", @@ -771,7 +771,7 @@ "title_normalized": "rocky horror trivia game" }, { - "title_raw": "ART NOUVEAU ARTISTRY", + "title_raw": "ARTISTRY", "confidence": "high", "publisher_hint": "Dux Somnium", "edition_hint": "", @@ -781,7 +781,7 @@ "source_photos": [ "IMG_4515.jpeg" ], - "title_normalized": "art nouveau artistry" + "title_normalized": "artistry" }, { "title_raw": "SPEECHLESS", @@ -902,7 +902,7 @@ "title_normalized": "meteor" }, { - "title_raw": "MAGE WARS ACADEMY CORE SET", + "title_raw": "MAGE WARS ACADEMY", "confidence": "high", "publisher_hint": "Arcane Wonders", "edition_hint": "Core Set", @@ -912,7 +912,7 @@ "source_photos": [ "IMG_4518.jpeg" ], - "title_normalized": "mage wars academy core set" + "title_normalized": "mage wars academy" }, { "title_raw": "DOCTOR WHO THE card GAME", diff --git a/src/bggpipe/diff.py b/src/bggpipe/diff.py index 9ba3cbc..ae49db9 100644 --- a/src/bggpipe/diff.py +++ b/src/bggpipe/diff.py @@ -135,6 +135,11 @@ def compute_diff(rows: list[dict], collection: list[CollectionItem]) -> DiffResu if status == "merged": result.merged += 1 # represented by its survivor row continue + if status == "local": + # the human's call: a real game BGG doesn't have — a library + # citizen only, never queued + result.local_only.append(row["title_raw"]) + continue if status not in RECOGNIZED_MATCH_STATUSES or not row["bgg_id"]: result.pending.append(row["title_raw"]) continue @@ -316,7 +321,9 @@ def run_diff(cfg: Config, *, client: BGGClient | None = None) -> DiffResult: 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 "" + f" · {len(result.local_only)} local-only (RPGs, off-BGG games)" + if result.local_only + else "" ) typer.echo( f"\n{result.recognized} recognized · {len(result.already_owned)} already " diff --git a/src/bggpipe/enrich.py b/src/bggpipe/enrich.py index eaa388f..2bd4f1d 100644 --- a/src/bggpipe/enrich.py +++ b/src/bggpipe/enrich.py @@ -28,7 +28,8 @@ from bggpipe.bgg_client import ( from bggpipe.config import Config from bggpipe.fsio import atomic_write_text from bggpipe.models import is_confident_version, is_recognized -from bggpipe.resolve import read_matches +from bggpipe.normalize import normalize_title +from bggpipe.resolve import load_titles, read_matches BATCH_SIZE = 20 @@ -95,12 +96,40 @@ def run_enrich( games[key] = {**fetched[bgg_id], "version": version} updated += 1 + # games the human ruled off-BGG: library entries built from their own + # photo reads — no API involved, so a blocked run still lands them + local_keys: set[str] = set() + local_rows = [r for r in rows if r["match_status"] == "local"] + if local_rows: + try: + cues = { + (e.title_raw, ";".join(e.source_photos)): e + for e in load_titles(cfg.titles_path) + } + except FileNotFoundError: + cues = {} + for row in local_rows: + key = f"local:{normalize_title(row['title_raw'])}:{row['source_photos']}" + local_keys.add(key) + entry = cues.get((row["title_raw"], row["source_photos"])) + games[key] = { + "bgg_id": None, + "name": row["title_raw"], + "year": entry.year_hint if entry else None, + "type": "localgame", + "publishers": [entry.publisher_hint] + if entry and entry.publisher_hint + else [], + "source_photos": [p for p in row["source_photos"].split(";") if p], + } + updated += 1 + # prune keys no current target claims: a row whose version was approved # after a bare-key run (or was later rejected) must not leave an orphan # entry in the frontend seed data. Only safe when nothing was blocked — # a token-less run knows too little to declare anything stale. if not blocked: - current = {key for key, _, _ in targets} + current = {key for key, _, _ in targets} | local_keys stale = [k for k in games if k not in current] for k in stale: del games[k] diff --git a/src/bggpipe/review.py b/src/bggpipe/review.py index 6e5b532..ccb0ab4 100644 --- a/src/bggpipe/review.py +++ b/src/bggpipe/review.py @@ -357,6 +357,17 @@ class ReviewSession: row["match_status"] = "rejected" self._save(row) + def decide_local(self, row: dict) -> None: + """A real game BGG simply doesn't have: a local library citizen — + listed and enriched from its own photo reads, never uploaded.""" + row["match_status"] = "local" + row["bgg_id"] = "" + row["bgg_name"] = "" + row["version_status"] = "" + row["version_id"] = "" + row["version_name"] = "" + self._save(row) + def open_version_ballot(self, row: dict) -> int: """The human knows which printing a box is even when the photo showed no cues: put EVERY published version on the row's ballot @@ -443,10 +454,10 @@ class ReviewSession: while True: self._show_item(row, candidates) prompt = ( - "[1-N] pick (s)kip (r)eject (m ) manual BGG id " - "(f ) re-search (q)uit > " + "[1-N] pick (s)kip (r)eject (l)ocal — not on BGG " + "(m ) manual BGG id (f ) re-search (q)uit > " if row["match_status"] == "unmatched" - else "[1-N] pick (s)kip (r)eject (q)uit > " + else "[1-N] pick (s)kip (r)eject (l)ocal — not on BGG (q)uit > " ) answer = self._ask(prompt) lowered = answer.lower() @@ -455,6 +466,9 @@ class ReviewSession: if lowered == "r": self.decide_reject(row) return + if lowered == "l": + self.decide_local(row) + return if answer.isdigit() and 1 <= int(answer) <= len(candidates): self.decide_pick(row, candidates[int(answer) - 1]) return diff --git a/src/bggpipe/static/app.js b/src/bggpipe/static/app.js index c97d837..a487b8b 100644 --- a/src/bggpipe/static/app.js +++ b/src/bggpipe/static/app.js @@ -155,6 +155,7 @@ function statusChip(c) { if (c.status === "awaiting_resolve") return `awaiting BGG`; if (c.status === "auto" || c.status === "approved") return `${esc(c.status)}`; if (c.status === "rejected") return `rejected`; + if (c.status === "local") return `local — not on BGG`; if (c.status === "merged") return `merged → ${esc(c.merged_into)}`; return `${esc(c.status)}`; } diff --git a/src/bggpipe/templates/pages/help.html b/src/bggpipe/templates/pages/help.html index 4379fe7..955467e 100644 --- a/src/bggpipe/templates/pages/help.html +++ b/src/bggpipe/templates/pages/help.html @@ -28,7 +28,7 @@

Pipeline — run stages one at a time and watch their live output. Shows what's blocking (missing keys, stub data) and the counts at every step.

Photos — drag photos in, drop them in the photos/ folder, or (on a paired phone) tap the drop zone and shoot straight from the camera; camera captures get unique shelf-… names so they can never overwrite each other. Each photo has its own page listing every title read from it and any reshoot tickets — boxes seen but not identified. Photograph those up close, drop the new shot in, and extract again. Re-uploading a photo under the same file name deliberately replaces it, and the next extract run re-reads it.

Titles — every read off your shelves, alphabetized, with its status and photos. This is the proofread checkpoint: edit, split, remove. Its badge counts shaky read lines — the model wasn't sure and nothing has verified them; filter to them, then press ✓ looks right or edit each one.

-

Review — the decisions only you can make: which game a title is, which edition a copy is, and whether two same-game reads are really one box (merges show a veto). Keyboard-first; see shortcuts.

+

Review — the decisions only you can make: which game a title is, which edition a copy is, whether two same-game reads are really one box (merges show a veto), and whether an unmatched title is a real game BGG simply doesn't have (keep locally: it joins the Library, never uploads). Keyboard-first; see shortcuts.

Queue — exactly what upload will do (new entries and version upgrades) and the log of everything it has done. Nothing reaches BGG that isn't visible here first.

Library — your enriched collection: filter by board games or RPGs. RPG matches are identified and enriched but never uploaded — BGG collections can't hold them, so they stay local citizens.

@@ -49,13 +49,14 @@

auto matched confidently, no review needed. approved you picked the match yourself.

ambiguous several plausible games — needs your pick on Review. unmatched nothing plausible found — enter a BGG id or re-search on Review.

merged two reads judged to be the same physical box; the merge is veto-able on Review. copy one copy of a title you split.

-

rejected you ruled it's not on BGG (or not a game worth matching); it stays listed but goes no further.

+

local — not on BGG you ruled it's a real game BGG doesn't have: it joins the Library from its own photo reads, and never uploads.

+

rejected you ruled it's a bad read or not worth matching; it stays listed but goes no further.

shaky read the vision model wasn't sure of this transcription and nothing has verified it yet — these are what the Titles badge counts. Clear one by pressing its ✓ looks right (the read is fine as-is) or by editing it (you fixed it). A BGG match also clears it: a wrong read wouldn't have matched.

Keyboard shortcuts

-

Review: j/k move between cards · 19 pick a candidate · r reject · m manual BGG id · u edition unknown · v veto a merge.

+

Review: j/k move between cards · 19 pick a candidate · r reject · l keep local (not on BGG) · m manual BGG id · u edition unknown · v veto a merge.

Photo pages: / move between photos.

diff --git a/src/bggpipe/templates/pages/library.html b/src/bggpipe/templates/pages/library.html index cc15f3d..d70bedd 100644 --- a/src/bggpipe/templates/pages/library.html +++ b/src/bggpipe/templates/pages/library.html @@ -27,7 +27,8 @@ function gameCard(g) { : ``}
${esc(g.name)}${g.year ? ` (${esc(g.year)})` : ""} - ${g.type === "rpgitem" ? `RPG · local only` : ""}
+ ${g.type === "rpgitem" ? `RPG · local only` : ""} + ${g.type === "localgame" ? `not on BGG · local` : ""}
${[players, time, weight, rank].filter(Boolean).map(esc).join(" · ")}
${g.version ? `
${esc(g.version.name || "")}
` : ""} diff --git a/src/bggpipe/templates/pages/review.html b/src/bggpipe/templates/pages/review.html index f201165..c9b9380 100644 --- a/src/bggpipe/templates/pages/review.html +++ b/src/bggpipe/templates/pages/review.html @@ -3,7 +3,7 @@ j/k move · 19 pick · - r reject · m manual id · u unknown · + r reject · l keep local · m manual id · u unknown · v veto merge @@ -76,6 +76,7 @@ function matchCard(row, idx) { m + `; @@ -179,6 +180,8 @@ function render() { }); m.querySelectorAll(".reject").forEach(b => b.onclick = () => decide(b.closest(".card"), "reject")); + m.querySelectorAll(".golocal").forEach(b => b.onclick = () => + decide(b.closest(".card"), "local")); m.querySelectorAll(".unknown").forEach(b => b.onclick = () => version(b.closest(".card"), "unknown")); m.querySelectorAll(".veto").forEach(b => b.onclick = () => @@ -238,6 +241,7 @@ document.addEventListener("keydown", e => { if (li) version(card, "pick", Number(li.dataset.pickver)); } else if (e.key === "r" && kind === "match") decide(card, "reject"); + else if (e.key === "l" && kind === "match") decide(card, "local"); else if (e.key === "u" && kind === "version") version(card, "unknown"); else if (e.key === "v" && kind === "merge") vetoMerge(card); else if (e.key === "m" && kind === "match") { card.querySelector("input")?.focus(); e.preventDefault(); } diff --git a/src/bggpipe/webreview.py b/src/bggpipe/webreview.py index 7d58fbc..0f85ff1 100644 --- a/src/bggpipe/webreview.py +++ b/src/bggpipe/webreview.py @@ -618,6 +618,7 @@ def create_app( "ambiguous": counts.get("ambiguous", 0), "unmatched": counts.get("unmatched", 0), "rejected": counts.get("rejected", 0), + "local": counts.get("local", 0), "version_updates": version_updates, "total": len(session.rows), "extracted": len(session.titles), @@ -898,6 +899,8 @@ def create_app( session.decide_manual(row, body.bgg_id) elif body.action == "reject": session.decide_reject(row) + elif body.action == "local": + session.decide_local(row) else: raise HTTPException(400, f"unknown action {body.action!r}") return state() diff --git a/tests/test_diff.py b/tests/test_diff.py index 8ab0aef..95cceb1 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -407,6 +407,14 @@ def test_rpgitem_rows_are_local_only_never_queued(): assert result.recognized == 1 # RPGs aren't counted against the queue +def test_local_rows_are_library_citizens_never_queued(): + local = {**_match("Obscure Homebrew", ""), "match_status": "local"} + result = compute_diff([local, _match("Catan", "13")], []) + assert result.local_only == ["Obscure Homebrew"] + assert [r["bgg_id"] for r in result.to_add] == ["13"] + assert result.pending == [] # decided, not waiting + + def test_split_copies_become_extra_collection_entries(): # three vetoed per-photo copies, one owned: one claims the copy, the # other two are queued as new entries diff --git a/tests/test_enrich.py b/tests/test_enrich.py index a34c0d4..c61a9b0 100644 --- a/tests/test_enrich.py +++ b/tests/test_enrich.py @@ -226,3 +226,55 @@ def test_enrich_degrades_without_token(tmp_path, capsys): assert games == {} assert "waiting on the BGG API" in capsys.readouterr().out assert json.loads((cfg.data_dir / "games.json").read_text()) == {} + + +def test_local_rows_enrich_from_their_own_reads(tmp_path): + cfg = Config(data_dir=tmp_path / "data") + cfg.data_dir.mkdir(parents=True) + write_matches( + cfg.matches_path, + [ + { + "title_raw": "Obscure Homebrew", + "bgg_id": "", + "bgg_name": "", + "year": "", + "type": "", + "match_status": "local", + "version_id": "", + "version_name": "", + "version_status": "", + "candidates_json": "[]", + "version_candidates_json": "[]", + "source_photos": "shelf.jpg", + } + ], + ) + cfg.titles_path.write_text( + json.dumps( + [ + { + "title_raw": "Obscure Homebrew", + "publisher_hint": "Basement Press", + "year_hint": 1998, + "source_photos": ["shelf.jpg"], + } + ] + ) + ) + from bggpipe.enrich import run_enrich + + client = BGGClient( + cache_dir=cfg.data_dir / "cache", + transport=httpx.MockTransport(_no_network), + ) + games = run_enrich(cfg, client=client) + (key,) = [k for k in games if k.startswith("local:")] + entry = games[key] + assert entry["name"] == "Obscure Homebrew" + assert entry["year"] == 1998 + assert entry["publishers"] == ["Basement Press"] + assert entry["type"] == "localgame" + # idempotent: a second run keeps the entry (no prune, no dupe) + games2 = run_enrich(cfg, client=client) + assert key in games2 diff --git a/tests/test_review.py b/tests/test_review.py index e76dcab..6c099e9 100644 --- a/tests/test_review.py +++ b/tests/test_review.py @@ -593,3 +593,15 @@ def test_open_version_ballot_puts_every_edition_up_for_review(tmp_path): ) with pytest.raises(ValueError, match="no BGG match"): session2.open_version_ballot(session2.rows[0]) + + +def test_local_decision_keeps_a_real_game_off_bgg(tmp_path): + cfg = _setup( + tmp_path, [_row(title_raw="Obscure Homebrew", match_status="unmatched")] + ) + run_review( + cfg, console=quiet_console(), input_fn=scripted("l"), client=fixture_client() + ) + (row,) = read_matches(cfg.matches_path) + assert row["match_status"] == "local" + assert row["bgg_id"] == "" # a local citizen carries no BGG identity diff --git a/tests/test_webreview.py b/tests/test_webreview.py index 7abca74..3ff7e45 100644 --- a/tests/test_webreview.py +++ b/tests/test_webreview.py @@ -1128,3 +1128,14 @@ def test_open_versions_endpoint_routes_row_into_review(tmp_path): json={"title_raw": "Dungeon!", "source_photos": "shelf.jpg"}, ) assert res.status_code == 502 + + +def test_local_decision_via_web(tmp_path): + web, cfg = make_client(tmp_path) + state = web.post( + "/api/decision", + json={"title_raw": "Mystery", "source_photos": "shelf.jpg", "action": "local"}, + ).json() + saved = {r["title_raw"]: r for r in read_matches(cfg.matches_path)} + assert saved["Mystery"]["match_status"] == "local" + assert state["summary"]["local"] == 1