diff --git a/CLAUDE.md b/CLAUDE.md index d5713b4..599a4bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,11 +47,11 @@ 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. -- **Human curation is durable**: `data/title_splits.json` (photo-scoped split-into-copies decisions, honored by extract's dedupe AND resolve's dedupe), `data/title_edits.json` (corrected reads/cues, applied before dedupe on every titles.json rebuild), `data/title_removals.json` (lines removed from the catalog — filtered out of every rebuild; delete the record to undo), and `data/title_additions.json` (games added without a photo — joined into every rebuild; a later photo sighting dedupe-merges with them) persist forever. Row-level decisions persist via the `dedupe_veto` column — edits never drop veto'd rows (a rename retitles them in place); removal drops them (explicitly discarding the line). -- **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). +- **Human curation is durable**: `data/title_splits.json` (photo-scoped split-into-copies decisions, honored by extract's dedupe AND resolve's dedupe), `data/title_edits.json` (corrected reads/cues, applied before dedupe on every titles.json rebuild), `data/title_removals.json` (lines removed from the catalog — filtered out of every rebuild; delete the record to undo), `data/title_additions.json` (games added without a photo — joined into every rebuild; a later photo sighting dedupe-merges with them), and `data/local_games.json` + `data/local_art/` (hand-written facts and a cover photo for off-BGG games — the ONLY source for them, merged over the photo reads by enrich) persist forever. Row-level decisions persist via the `dedupe_veto` column — edits never drop veto'd rows (a rename retitles them in place); removal drops them (explicitly discarding the line). +- **RPGs are local-only citizens**: when the board-game search runs dry, resolve falls back to `type=rpgitem` (same geekdo API/token). RPGGeek items carry their OWN link types (`rpgdesigner`, `rpgpublisher`, `rpggenre`, `rpgcategory`, `rpgmechanic`) — a board-game-only parser silently returns nothing for them. 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 - Remote is self-hosted Gitea 1.26 (`git.kestrelsnest.social/eric/bggpipe`), **not GitHub** — `gh` CLI does not work here. -- Commit `data/matches.csv`, `data/to_add.csv`, `data/to_update.csv`, `data/upload_log.csv`, `data/titles.json`, `data/unidentified.json`, `data/unidentified_dismissed.json`, `data/title_splits.json`, `data/title_edits.json`, `data/title_removals.json`, `data/title_additions.json`, `data/games.json`, `data/STUB_DATA.marker` (while it applies), and the collection snapshot XMLs. Never commit `data/bgg_cache/`, `data/extract_raw/`, `photos/`, `data/.lan_key`, Playwright storage state, or `.env`. +- Commit `data/matches.csv`, `data/to_add.csv`, `data/to_update.csv`, `data/upload_log.csv`, `data/titles.json`, `data/unidentified.json`, `data/unidentified_dismissed.json`, `data/title_splits.json`, `data/title_edits.json`, `data/title_removals.json`, `data/title_additions.json`, `data/local_games.json`, `data/local_art/`, `data/games.json`, `data/STUB_DATA.marker` (while it applies), and the collection snapshot XMLs. Never commit `data/bgg_cache/`, `data/extract_raw/`, `photos/`, `data/.lan_key`, Playwright storage state, or `.env`. diff --git a/data/games.json b/data/games.json index 2c222b2..19dfdb5 100644 --- a/data/games.json +++ b/data/games.json @@ -36,6 +36,8 @@ "Open Drafting", "Set Collection" ], + "producers": [], + "series": [], "rating": 7.18364, "weight": 2.2411, "rank": 1534, @@ -81,6 +83,8 @@ "mechanics": [ "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.20545, "weight": 2.6406, "rank": 3075, @@ -150,6 +154,8 @@ "Role Playing", "Set Collection" ], + "producers": [], + "series": [], "rating": 7.04319, "weight": 1.6589, "rank": 622, @@ -205,6 +211,8 @@ "Real-Time", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 6.8968, "weight": 1.1262, "rank": 1133, @@ -268,6 +276,8 @@ "Stat Check Resolution", "Voting" ], + "producers": [], + "series": [], "rating": 7.18247, "weight": 3.1602, "rank": 1210, @@ -342,6 +352,8 @@ "Trading", "Turn Order: Stat-Based" ], + "producers": [], + "series": [], "rating": 7.48444, "weight": 3.6421, "rank": 540, @@ -408,6 +420,8 @@ "Set Collection", "Trading" ], + "producers": [], + "series": [], "rating": 8.00875, "weight": 3.6547, "rank": null, @@ -458,6 +472,8 @@ "mechanics": [ "Set Collection" ], + "producers": [], + "series": [], "rating": 5.38519, "weight": 2.0417, "rank": 28664, @@ -519,6 +535,8 @@ "Rock-Paper-Scissors", "Simulation" ], + "producers": [], + "series": [], "rating": 5.66004, "weight": 2.6897, "rank": 25110, @@ -572,6 +590,8 @@ "Hexagon Grid", "Simulation" ], + "producers": [], + "series": [], "rating": 6.42178, "weight": 3.4444, "rank": 10357, @@ -627,6 +647,8 @@ "Solo / Solitaire Game", "Tile Placement" ], + "producers": [], + "series": [], "rating": 7.62134, "weight": 1.1038, "rank": 972, @@ -687,6 +709,8 @@ "Three Dimensional Movement", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.32924, "weight": 1.7186, "rank": 315, @@ -727,6 +751,8 @@ "Hand Management", "Simultaneous Action Selection" ], + "producers": [], + "series": [], "rating": 5.38127, "weight": 1.0, "rank": 29645, @@ -781,6 +807,8 @@ "Player Judge", "Simultaneous Action Selection" ], + "producers": [], + "series": [], "rating": 5.69812, "weight": 1.1716, "rank": 17609, @@ -834,6 +862,8 @@ "Player Judge", "Simultaneous Action Selection" ], + "producers": [], + "series": [], "rating": 6.20464, "weight": 1.0291, "rank": 3574, @@ -877,6 +907,8 @@ "mechanics": [ "Hand Management" ], + "producers": [], + "series": [], "rating": 5.66383, "weight": 1.1911, "rank": 13616, @@ -949,6 +981,8 @@ "Set Collection", "Take That" ], + "producers": [], + "series": [], "rating": 6.06848, "weight": 1.0792, "rank": 4015, @@ -993,6 +1027,8 @@ "Modular Board", "Player Elimination" ], + "producers": [], + "series": [], "rating": 6.86503, "weight": 2.0, "rank": 3049, @@ -1045,6 +1081,8 @@ "mechanics": [ "Line Drawing" ], + "producers": [], + "series": [], "rating": 7.09873, "weight": 1.0, "rank": 3385, @@ -1098,6 +1136,8 @@ "Simulation", "Variable Set-up" ], + "producers": [], + "series": [], "rating": 7.35841, "weight": 4.2286, "rank": 3295, @@ -1177,6 +1217,8 @@ "Simulation", "Simultaneous Action Selection" ], + "producers": [], + "series": [], "rating": 7.03418, "weight": 3.3234, "rank": 888, @@ -1224,6 +1266,8 @@ "Auction / Bidding", "Simulation" ], + "producers": [], + "series": [], "rating": 7.08668, "weight": 3.85, "rank": 2125, @@ -1282,6 +1326,8 @@ "Dice Rolling", "Hand Management" ], + "producers": [], + "series": [], "rating": 7.11159, "weight": 2.7649, "rank": 1274, @@ -1313,11 +1359,30 @@ "min_playtime": null, "max_playtime": null, "min_age": null, - "designers": [], - "artists": [], - "publishers": [], - "categories": [], - "mechanics": [], + "designers": [ + "John Battle" + ], + "artists": [ + "Micah Anderson", + "Kate Sheridan", + "Jared Sinclair" + ], + "publishers": [ + "Project Nerves" + ], + "categories": [ + "Fantasy", + "Core Rules (min needed to play)" + ], + "mechanics": [ + "Attribute/Stat Based (STR, CON, PER, etc)", + "Dice (Various)", + "Dice Step (variable dice vs. static target)" + ], + "producers": [ + "John Battle" + ], + "series": [], "rating": 0.0, "weight": 0.0, "rank": null, @@ -1384,6 +1449,8 @@ "Push Your Luck", "Set Collection" ], + "producers": [], + "series": [], "rating": 7.3845, "weight": 1.8187, "rank": 262, @@ -1511,6 +1578,8 @@ "Trading", "Variable Set-up" ], + "producers": [], + "series": [], "rating": 7.09057, "weight": 2.2809, "rank": 626, @@ -1565,6 +1634,8 @@ "Stacking and Balancing", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 6.82201, "weight": 1.4753, "rank": 1362, @@ -1617,6 +1688,8 @@ "Square Grid", "Static Capture" ], + "producers": [], + "series": [], "rating": 7.33418, "weight": 1.6583, "rank": 362, @@ -1668,6 +1741,8 @@ "Set Collection", "Solo / Solitaire Game" ], + "producers": [], + "series": [], "rating": 6.98509, "weight": 1.2235, "rank": 1213, @@ -1736,6 +1811,8 @@ "Slide / Push", "Square Grid" ], + "producers": [], + "series": [], "rating": 6.4106, "weight": 1.3468, "rank": 2116, @@ -1791,6 +1868,8 @@ "Dice Rolling", "Set Collection" ], + "producers": [], + "series": [], "rating": 5.83169, "weight": 1.75, "rank": 22810, @@ -1836,6 +1915,8 @@ "Roll / Spin and Move", "Variable Set-up" ], + "producers": [], + "series": [], "rating": 6.01372, "weight": 1.0526, "rank": 5588, @@ -1909,6 +1990,8 @@ "Set Collection", "Solo / Solitaire Game" ], + "producers": [], + "series": [], "rating": 5.73224, "weight": 1.2295, "rank": 7891, @@ -1966,6 +2049,8 @@ "Trading", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.2278, "weight": 2.0, "rank": null, @@ -2088,6 +2173,8 @@ "Single Loser Game", "Stacking and Balancing" ], + "producers": [], + "series": [], "rating": 5.66472, "weight": 1.1, "rank": 10481, @@ -2133,6 +2220,8 @@ "Role Playing", "Storytelling" ], + "producers": [], + "series": [], "rating": 7.34252, "weight": 1.7, "rank": 5792, @@ -2170,6 +2259,8 @@ "mechanics": [ "Dice Rolling" ], + "producers": [], + "series": [], "rating": 5.5, "weight": 0.0, "rank": null, @@ -2213,6 +2304,8 @@ "Programmed Movement", "Slide / Push" ], + "producers": [], + "series": [], "rating": 5.92143, "weight": 1.3415, "rank": 7729, @@ -2265,6 +2358,8 @@ "Team-Based Game", "Voting" ], + "producers": [], + "series": [], "rating": 4.88512, "weight": 1.2, "rank": 30462, @@ -2315,6 +2410,8 @@ "Simultaneous Action Selection", "Take That" ], + "producers": [], + "series": [], "rating": 6.29365, "weight": 1.0, "rank": 8130, @@ -2359,6 +2456,8 @@ "mechanics": [ "Voting" ], + "producers": [], + "series": [], "rating": 5.81063, "weight": 1.1791, "rank": 9918, @@ -2408,6 +2507,8 @@ "Team-Based Game", "Trick-taking" ], + "producers": [], + "series": [], "rating": 6.44649, "weight": 1.8711, "rank": 3154, @@ -2454,6 +2555,8 @@ "mechanics": [ "Point to Point Movement" ], + "producers": [], + "series": [], "rating": 5.36558, "weight": 2.234, "rank": 29937, @@ -2506,6 +2609,8 @@ "Set Collection", "Take That" ], + "producers": [], + "series": [], "rating": 6.54128, "weight": 1.2685, "rank": 1630, @@ -2551,6 +2656,8 @@ "Trading", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 6.03087, "weight": 1.84, "rank": 14181, @@ -2603,6 +2710,8 @@ "Pick-up and Deliver", "Team-Based Game" ], + "producers": [], + "series": [], "rating": 6.83745, "weight": 1.3462, "rank": 1217, @@ -2654,6 +2763,8 @@ "Solo / Solitaire Game", "Trading" ], + "producers": [], + "series": [], "rating": 6.65415, "weight": 1.6574, "rank": 1422, @@ -2723,6 +2834,8 @@ "Take That", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 6.54184, "weight": 1.6601, "rank": 2164, @@ -2798,6 +2911,8 @@ "Targeted Clues", "Voting" ], + "producers": [], + "series": [], "rating": 7.17927, "weight": 1.1915, "rank": 433, @@ -2850,6 +2965,8 @@ "Roll / Spin and Move", "Spelling" ], + "producers": [], + "series": [], "rating": 5.35741, "weight": 1.6296, "rank": 30388, @@ -2889,6 +3006,8 @@ "Memory", "Team-Based Game" ], + "producers": [], + "series": [], "rating": 3.7, "weight": 0.0, "rank": null, @@ -2926,6 +3045,8 @@ "mechanics": [ "Acting" ], + "producers": [], + "series": [], "rating": 6.7084, "weight": 1.25, "rank": 10370, @@ -2970,6 +3091,8 @@ "Dice Rolling", "Network and Route Building" ], + "producers": [], + "series": [], "rating": 6.57312, "weight": 1.4961, "rank": 2616, @@ -3020,6 +3143,8 @@ "Pattern Building", "Roll / Spin and Move" ], + "producers": [], + "series": [], "rating": 4.27778, "weight": 0.0, "rank": null, @@ -3067,6 +3192,8 @@ "Team-Based Game", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 5.99149, "weight": 2.0, "rank": 20853, @@ -3108,6 +3235,8 @@ "Humor" ], "mechanics": [], + "producers": [], + "series": [], "rating": 4.76695, "weight": 1.0, "rank": 30933, @@ -3157,6 +3286,8 @@ "Take That", "Turn Order: Progressive" ], + "producers": [], + "series": [], "rating": 6.51675, "weight": 1.2619, "rank": 3700, @@ -3230,6 +3361,8 @@ "Turn Order: Claim Action", "Worker Placement" ], + "producers": [], + "series": [], "rating": 7.85539, "weight": 3.6356, "rank": 64, @@ -3286,6 +3419,8 @@ "Hand Management", "Real-Time" ], + "producers": [], + "series": [], "rating": 6.04678, "weight": 1.8571, "rank": 10371, @@ -3328,6 +3463,8 @@ "Secret Unit Deployment", "Take That" ], + "producers": [], + "series": [], "rating": 5.67458, "weight": 1.9706, "rank": 14593, @@ -3369,6 +3506,8 @@ "mechanics": [ "Push Your Luck" ], + "producers": [], + "series": [], "rating": 5.43456, "weight": 1.265, "rank": 30199, @@ -3444,6 +3583,8 @@ "Storytelling", "Voting" ], + "producers": [], + "series": [], "rating": 7.18958, "weight": 1.8899, "rank": 454, @@ -3496,6 +3637,8 @@ "Hand Management", "Tile Placement" ], + "producers": [], + "series": [], "rating": 6.6071, "weight": 1.9481, "rank": 3416, @@ -3588,6 +3731,8 @@ "Square Grid", "Tile Placement" ], + "producers": [], + "series": [], "rating": 7.41679, "weight": 1.8844, "rank": 240, @@ -3636,6 +3781,8 @@ "Hand Management", "Simulation" ], + "producers": [], + "series": [], "rating": 6.56457, "weight": 1.7959, "rank": 3113, @@ -3683,6 +3830,8 @@ "Solo / Solitaire Game", "Storytelling" ], + "producers": [], + "series": [], "rating": 7.46966, "weight": 2.6875, "rank": 2680, @@ -3720,6 +3869,8 @@ "mechanics": [ "Pattern Recognition" ], + "producers": [], + "series": [], "rating": 5.69803, "weight": 1.0, "rank": 23981, @@ -3754,6 +3905,8 @@ "Deduction", "Team-Based Game" ], + "producers": [], + "series": [], "rating": 1.0, "weight": 0.0, "rank": null, @@ -3794,6 +3947,8 @@ "mechanics": [ "Dice Rolling" ], + "producers": [], + "series": [], "rating": 5.5063, "weight": 1.0, "rank": 28676, @@ -3851,6 +4006,8 @@ "Solo / Solitaire Game", "Storytelling" ], + "producers": [], + "series": [], "rating": 8.29227, "weight": 2.9538, "rank": 826, @@ -3903,6 +4060,8 @@ "Modular Board", "Resource to Move" ], + "producers": [], + "series": [], "rating": 8.15214, "weight": 3.0, "rank": 8928, @@ -3974,6 +4133,8 @@ "Tile Placement", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 6.74848, "weight": 1.7404, "rank": 1035, @@ -4038,6 +4199,8 @@ "Memory", "Push Your Luck" ], + "producers": [], + "series": [], "rating": 6.01617, "weight": 1.2069, "rank": 7692, @@ -4104,6 +4267,8 @@ "Hand Management", "Set Collection" ], + "producers": [], + "series": [], "rating": 5.67589, "weight": 1.3874, "rank": 12263, @@ -4164,6 +4329,8 @@ "Real-Time", "Team-Based Game" ], + "producers": [], + "series": [], "rating": 6.63882, "weight": 1.1111, "rank": 2389, @@ -4222,6 +4389,8 @@ "Single Loser Game", "Trick-taking" ], + "producers": [], + "series": [], "rating": 5.97587, "weight": 1.0909, "rank": 12119, @@ -4290,6 +4459,8 @@ "Take That", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.90234, "weight": 2.5, "rank": null, @@ -4342,6 +4513,8 @@ "Modular Board", "Player Elimination" ], + "producers": [], + "series": [], "rating": 6.755, "weight": 2.0, "rank": 11443, @@ -4391,6 +4564,8 @@ "Campaign / Battle Card Driven", "Dice Rolling" ], + "producers": [], + "series": [], "rating": 5.94534, "weight": 2.2695, "rank": 5594, @@ -4444,6 +4619,8 @@ "Take That", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.3575, "weight": 0.0, "rank": null, @@ -4523,6 +4700,8 @@ "Solo / Solitaire Game", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 8.72221, "weight": 4.4078, "rank": 21, @@ -4601,6 +4780,8 @@ "Tags", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 8.5343, "weight": 3.9191, "rank": 4, @@ -4638,6 +4819,8 @@ "mechanics": [ "Paper-and-Pencil" ], + "producers": [], + "series": [], "rating": 7.7, "weight": 0.0, "rank": null, @@ -4689,6 +4872,8 @@ "Tile Placement", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.29628, "weight": 2.6377, "rank": 1045, @@ -4758,6 +4943,8 @@ "Variable Set-up", "Worker Placement" ], + "producers": [], + "series": [], "rating": 8.76449, "weight": 3.0988, "rank": null, @@ -4822,6 +5009,8 @@ "Tile Placement", "Variable Set-up" ], + "producers": [], + "series": [], "rating": 7.25885, "weight": 2.0703, "rank": 761, @@ -4887,6 +5076,8 @@ "Solo / Solitaire Game", "Tile Placement" ], + "producers": [], + "series": [], "rating": 7.47529, "weight": 2.1972, "rank": 273, @@ -4951,6 +5142,8 @@ "Solo / Solitaire Game", "Storytelling" ], + "producers": [], + "series": [], "rating": 8.08218, "weight": 3.2622, "rank": 75, @@ -5017,6 +5210,8 @@ "Push Your Luck", "Storytelling" ], + "producers": [], + "series": [], "rating": 8.51983, "weight": 3.1429, "rank": null, @@ -5082,6 +5277,8 @@ "Storytelling", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.83158, "weight": 3.5797, "rank": 672, @@ -5141,6 +5338,8 @@ "Solo / Solitaire Game", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 6.98235, "weight": 2.0645, "rank": 3679, @@ -5211,6 +5410,8 @@ "Storytelling", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.47997, "weight": 3.3212, "rank": 887, @@ -5300,6 +5501,8 @@ "Take That", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 5.87399, "weight": 1.8183, "rank": 6517, @@ -5357,6 +5560,8 @@ "mechanics": [ "Modular Board" ], + "producers": [], + "series": [], "rating": 5.36581, "weight": 1.3019, "rank": 30370, @@ -5411,6 +5616,8 @@ "Rock-Paper-Scissors", "Simultaneous Action Selection" ], + "producers": [], + "series": [], "rating": 4.62467, "weight": 1.1515, "rank": 30770, @@ -5497,6 +5704,8 @@ "Solo / Solitaire Game", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 8.35383, "weight": 3.6415, "rank": 12, @@ -5543,6 +5752,8 @@ "Real-Time", "Spelling" ], + "producers": [], + "series": [], "rating": 5.14003, "weight": 1.18, "rank": 30955, @@ -5610,6 +5821,8 @@ "Re-rolling and Locking", "Set Collection" ], + "producers": [], + "series": [], "rating": 5.68545, "weight": 1.1146, "rank": 12237, @@ -5655,6 +5868,8 @@ "Party Game" ], "mechanics": [], + "producers": [], + "series": [], "rating": 5.97667, "weight": 1.0, "rank": 26208, @@ -5700,6 +5915,8 @@ "Hand Management", "Push Your Luck" ], + "producers": [], + "series": [], "rating": 5.46742, "weight": 1.3636, "rank": 27659, @@ -5741,6 +5958,8 @@ "Set Collection", "Take That" ], + "producers": [], + "series": [], "rating": 6.37219, "weight": 1.625, "rank": 7977, @@ -5807,6 +6026,8 @@ "Role Playing", "Set Collection" ], + "producers": [], + "series": [], "rating": 7.51828, "weight": 2.0, "rank": null, @@ -5880,6 +6101,8 @@ "Race", "Random Production" ], + "producers": [], + "series": [], "rating": 6.59134, "weight": 1.519, "rank": 1392, @@ -5929,6 +6152,8 @@ "mechanics": [ "Storytelling" ], + "producers": [], + "series": [], "rating": 6.90986, "weight": 1.4545, "rank": 10937, @@ -5983,6 +6208,8 @@ "Auction / Bidding", "Hand Management" ], + "producers": [], + "series": [], "rating": 5.83695, "weight": 1.2727, "rank": 8096, @@ -6045,6 +6272,8 @@ "Open Drafting", "Point to Point Movement" ], + "producers": [], + "series": [], "rating": 6.1564, "weight": 1.6417, "rank": 3581, @@ -6096,6 +6325,8 @@ "Deduction", "Hand Management" ], + "producers": [], + "series": [], "rating": 8.45244, "weight": 3.0, "rank": null, @@ -6127,11 +6358,50 @@ "min_playtime": null, "max_playtime": null, "min_age": null, - "designers": [], - "artists": [], - "publishers": [], - "categories": [], - "mechanics": [], + "designers": [ + "Jared A. Sorensen" + ], + "artists": [ + "Alexey Andreyev", + "Samuel Araya", + "Rebekah Bennington", + "Alice K. Hansmann", + "Todd James", + "Manning Krull", + "Jon Morris", + "Keith Senkowski", + "Lukas Wong-Achorn", + "Jordan Worley", + "Ariel ZB" + ], + "publishers": [ + "Memento Mori Theatricks" + ], + "categories": [ + "Action / Adventure", + "Comedy / Satire", + "Fantasy", + "History (American West)", + "Horror", + "Horror (Supernatural)", + "Horror (Zombies)", + "Kid-Friendly", + "Modern", + "Science Fiction", + "Weird / Surreal", + "Core Rules (min needed to play)", + "Scenario / Adventure / Module" + ], + "mechanics": [ + "Description Based (Narrative more so than Dice)", + "'Diceless' (does not use dice or other randomizer)", + "Reading (for Periodicals, Adventure Books, etc)" + ], + "producers": [ + "Luke Crane", + "Radosław Drozdalski" + ], + "series": [], "rating": 9.1875, "weight": 2.3333, "rank": null, @@ -6180,6 +6450,8 @@ "Melding and Splaying", "Point to Point Movement" ], + "producers": [], + "series": [], "rating": 5.99026, "weight": 1.7619, "rank": 6006, @@ -6233,6 +6505,8 @@ "Hand Management", "Simultaneous Action Selection" ], + "producers": [], + "series": [], "rating": 6.57926, "weight": 1.2857, "rank": 9241, @@ -6280,6 +6554,8 @@ "Trivia" ], "mechanics": [], + "producers": [], + "series": [], "rating": 4.55813, "weight": 1.3333, "rank": 30823, @@ -6322,6 +6598,8 @@ "mechanics": [ "Paper-and-Pencil" ], + "producers": [], + "series": [], "rating": 5.64883, "weight": 1.1429, "rank": 22992, @@ -6388,6 +6666,8 @@ "Pattern Recognition", "Speed Matching" ], + "producers": [], + "series": [], "rating": 5.97901, "weight": 1.0656, "rank": 6149, @@ -6421,6 +6701,8 @@ "Trivia" ], "mechanics": [], + "producers": [], + "series": [], "rating": 5.02273, "weight": 0.0, "rank": null, @@ -6502,6 +6784,8 @@ "Turn Order: Time Track", "Victory Points as a Resource" ], + "producers": [], + "series": [], "rating": 7.57737, "weight": 1.5967, "rank": 148, @@ -6553,6 +6837,8 @@ "Traitor Game", "Variable Set-up" ], + "producers": [], + "series": [], "rating": 7.13155, "weight": 2.7143, "rank": 2384, @@ -6614,6 +6900,8 @@ "Variable Set-up", "Victory Points as a Resource" ], + "producers": [], + "series": [], "rating": 7.19036, "weight": 2.3462, "rank": 410, @@ -6664,6 +6952,8 @@ "Open Drafting", "Set Collection" ], + "producers": [], + "series": [], "rating": 7.22669, "weight": 2.0, "rank": 6249, @@ -6719,6 +7009,8 @@ "Dice Rolling", "Variable Player Powers" ], + "producers": [], + "series": [], "rating": 7.08112, "weight": 3.1212, "rank": 2932, @@ -6772,6 +7064,8 @@ "Events", "Investment" ], + "producers": [], + "series": [], "rating": 7.77652, "weight": 0.0, "rank": null, @@ -6826,6 +7120,8 @@ "Investment", "Take That" ], + "producers": [], + "series": [], "rating": 7.69583, "weight": 0.0, "rank": null, @@ -6887,6 +7183,8 @@ "Set Collection", "Trading" ], + "producers": [], + "series": [], "rating": 7.81183, "weight": 3.2027, "rank": null, @@ -6920,6 +7218,8 @@ "Expansion for Base-game" ], "mechanics": [], + "producers": [], + "series": [], "rating": 7.75, "weight": 0.0, "rank": null, @@ -6973,6 +7273,8 @@ "Dice Rolling", "Hand Management" ], + "producers": [], + "series": [], "rating": 7.43776, "weight": 2.0661, "rank": null, @@ -7040,6 +7342,8 @@ "Take That", "Variable Set-up" ], + "producers": [], + "series": [], "rating": 7.05924, "weight": 2.3187, "rank": 1266, diff --git a/src/bggpipe/config.py b/src/bggpipe/config.py index 0b358a9..84bb0ca 100644 --- a/src/bggpipe/config.py +++ b/src/bggpipe/config.py @@ -74,6 +74,17 @@ class Config: def games_path(self) -> Path: return self.data_dir / "games.json" + @property + def local_games_path(self) -> Path: + # hand-written metadata for games BGG doesn't have — the only + # source of truth for them, so it is committed like the other stores + return self.data_dir / "local_games.json" + + @property + def local_art_dir(self) -> Path: + # cover photos for off-BGG games (committed: nothing else has them) + return self.data_dir / "local_art" + @property def title_additions_path(self) -> Path: # games the human added without a photo (expansions stored inside diff --git a/src/bggpipe/enrich.py b/src/bggpipe/enrich.py index cbc34f5..48ad49c 100644 --- a/src/bggpipe/enrich.py +++ b/src/bggpipe/enrich.py @@ -100,6 +100,17 @@ def run_enrich( # 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"] + # hand-written metadata wins over the photo reads: for an off-BGG game + # it is the only real source there is + hand: dict = {} + if cfg.local_games_path.exists(): + try: + hand = json.loads(cfg.local_games_path.read_text()) + except json.JSONDecodeError as err: + raise ValueError( + f"{cfg.local_games_path} is corrupt ({err}) — it holds " + "hand-written game data, so check git history before deleting" + ) from err if local_rows: try: cues = { @@ -122,6 +133,9 @@ def run_enrich( else [], "source_photos": [p for p in row["source_photos"].split(";") if p], } + games[key].update( + {k: v for k, v in (hand.get(key) or {}).items() if v not in (None, "")} + ) # 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 diff --git a/src/bggpipe/models.py b/src/bggpipe/models.py index 1d490e7..17d5608 100644 --- a/src/bggpipe/models.py +++ b/src/bggpipe/models.py @@ -232,11 +232,17 @@ def parse_things_full(xml_text: str) -> list[dict]: "min_playtime": _attr_int(item.find("minplaytime")), "max_playtime": _attr_int(item.find("maxplaytime")), "min_age": _attr_int(item.find("minage")), - "designers": links("boardgamedesigner"), - "artists": links("boardgameartist"), - "publishers": links("boardgamepublisher"), - "categories": links("boardgamecategory"), - "mechanics": links("boardgamemechanic"), + # RPGGeek items live in the same database but use their own + # link types, so a board-game-only reader finds none of them + "designers": links("boardgamedesigner") + links("rpgdesigner"), + "artists": links("boardgameartist") + links("rpgartist"), + "publishers": links("boardgamepublisher") + links("rpgpublisher"), + "categories": links("boardgamecategory") + + links("rpggenre") + + links("rpgcategory"), + "mechanics": links("boardgamemechanic") + links("rpgmechanic"), + "producers": links("rpgproducer"), + "series": links("rpgseries"), "rating": _attr_float(ratings.find("average")) if ratings is not None else None, diff --git a/src/bggpipe/static/app.css b/src/bggpipe/static/app.css index f463f21..7563722 100644 --- a/src/bggpipe/static/app.css +++ b/src/bggpipe/static/app.css @@ -487,6 +487,13 @@ a.game:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } } .chiplist { display: inline-flex; flex-wrap: wrap; gap: .3rem; vertical-align: middle; } .gdesc { white-space: pre-wrap; line-height: 1.6; } +.artbtn { margin-top: .6rem; width: 100%; font-size: .8rem; } +.editform label.wide { flex: 1 1 100%; } +.editform textarea { + font: inherit; font-size: .85rem; color: var(--ink); width: 100%; + border: 2px solid var(--board-edge); border-radius: var(--radius); + padding: .35rem .45rem; background: #fff; resize: vertical; +} .empty { background: var(--board); border: 2px dashed var(--board-edge); diff --git a/src/bggpipe/templates/pages/help.html b/src/bggpipe/templates/pages/help.html index 248c85d..de4e898 100644 --- a/src/bggpipe/templates/pages/help.html +++ b/src/bggpipe/templates/pages/help.html @@ -30,7 +30,7 @@
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, 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. A job that fails is skipped by later runs (so one broken game can't loop forever); when any exist, the Pipeline's upload card offers a retry N failed checkbox. Each queued row shows what upload did with it — pending, done, failed, or retired (a review decision since the last diff withdrew it). Finished rows stay listed until the next diff rebuilds the queue; the log below them is the permanent record.
-Library — your enriched collection. Search titles, designers, mechanics and categories at once; filter by kind (board games, RPGs, off-BGG) or by how many people are playing tonight; sort by name, year, BGG rank, weight, or playing time. Click any game for its full detail: art, the usual stats, designers and mechanics, your edition, the shelf photos it was read from, and a link to its BGG page. RPG and off-BGG games live here too — identified and enriched, never uploaded.
+Library — your enriched collection. Search titles, designers, mechanics and categories at once; filter by kind (board games, RPGs, off-BGG) or by how many people are playing tonight; sort by name, year, BGG rank, weight, or playing time. Click any game for its full detail: art, the usual stats, designers and mechanics, your edition, the shelf photos it was read from, and a link to its BGG page. RPG and off-BGG games live here too — identified and enriched, never uploaded. RPGs pull their designers, publishers and genres from RPGGeek; an off-BGG game's detail page lets you write its facts yourself and add a cover photo, since nothing else will ever have them (both are saved under data/ and folded in by the next enrich).
${esc(g.description)}