Two-lane packing, and the shelves ship with the export

Eric's wall answered the orientation question with a screenshot full
of red: the capacity model assumed one flat stack per opening, so his
spine-out cubes read 200% full. His reviewer's prescription, built to
its acceptance cases: each opening packs a FLAT lane (thinnest axes
against interior height, claiming the widest flat box's width) and a
STANDING lane beside it (boxes on edge against the remaining width;
standing boxes must fit height and depth upright). The largest-
footprint class lies flat, smaller boxes stand, any box's lane is
flippable per-assignment (▬/▮ toggle in the opening view, ✱ marks an
override), and ⚠ now means NO packing fits — not merely "tall stack".
Unmeasured boxes take no lane but keep their honesty tag; dual fill
bars show each lane's budget; a broken opening is never offered by
the suggester. On the real wall: the double-wides and half the cubes
went green (7 games = 13.12" flat + 2.91" standing), and the
remaining warnings mark cubes that genuinely hold 12-16 boxes.
Both reviewer acceptance tests pass verbatim.

And the export gains the humanity Eric asked for: game pages say
where each box lives, containers list their contents, and a shelves
page draws the wall as it physically stands — proportional cells,
zones, descriptions, every opening linking its residents. Publishing
a shelf layout is a choice: --no-shelves keeps the layer out.
"shelves" joins "art" as a reserved slug. 387 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-09 14:53:42 -04:00
co-authored by Claude Fable 5
parent d6ff5e73e9
commit e90a53fb6c
20 changed files with 1930 additions and 59 deletions
+6 -3
View File
@@ -25,7 +25,8 @@
"zone": "party",
"width_in": 13.25,
"height_in": 13.25,
"depth_in": 15.4
"depth_in": 15.4,
"description": "big boxes a-m"
},
{
"id": "library-kallax-b2",
@@ -33,7 +34,8 @@
"zone": "party",
"width_in": 13.25,
"height_in": 13.25,
"depth_in": 15.4
"depth_in": 15.4,
"description": "big boxes n-z"
},
{
"id": "library-kallax-b3",
@@ -41,7 +43,8 @@
"zone": "party",
"width_in": 13.25,
"height_in": 13.25,
"depth_in": 15.4
"depth_in": 15.4,
"description": "card rows standing"
},
{
"id": "library-kallax-b4",
+563
View File
@@ -10862,5 +10862,568 @@
"weight_lb": null,
"source": "version"
}
},
"291130:475387": {
"bgg_id": 291130,
"type": "boardgameexpansion",
"name": "Etherfields: 5th Player Expansion",
"year": 2020,
"description": "This expansion allows you to add a 5th player.\n\nThis is not only a 5th player expansion, but also an additional character to choose from for any other player count game. We called this character archetype Reaper.\n\nThis archetype will be based on wounds that this character has taken - the more, the better combos she will be able to execute. There will be multiple ways of using this to your advantage - either by full-on risky play or perhaps more focused on balance and getting a good healing deck build. The Reaper will provide quite interesting mechanics for the whole team!\n\n—description from the publisher",
"image": "https://cf.geekdo-images.com/irNTpn32rkT1BbR0cvbcvg__original/img/ASaUeBCashp4Y2PtS1NFMmxaAh0=/0x0/filters:format(jpeg)/pic5792667.jpg",
"thumbnail": "https://cf.geekdo-images.com/irNTpn32rkT1BbR0cvbcvg__small/img/6TczoQDunTYpVZAU6pAf_FzCsW8=/fit-in/200x150/filters:strip_icc()/pic5792667.jpg",
"min_players": 1,
"max_players": 5,
"best_player_counts": [
"1",
"2"
],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 14,
"designers": [],
"artists": [
"Ewa Labak",
"Michał Sałata"
],
"publishers": [
"Albi",
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [
"Cooperative Game",
"Deck, Bag, and Pool Building"
],
"producers": [],
"series": [],
"rating": 8.08151,
"weight": 3.0,
"rank": null,
"users_owned": 4083,
"version": {
"version_id": 475387,
"name": "English edition",
"year": 2020,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
],
"width_in": 12.2047,
"length_in": 12.2047,
"depth_in": 1.5748,
"weight_lb": 1.67551
},
"stored_in": null,
"dims": {
"width_in": 12.2047,
"length_in": 12.2047,
"depth_in": 1.5748,
"weight_lb": 1.67551,
"source": "version"
}
},
"355873:597861": {
"bgg_id": 355873,
"type": "boardgameexpansion",
"name": "Etherfields: Awakened Items & Actions",
"year": 2022,
"description": "Originally available as part of Pack D from the Dice Tower 2021 Kickstarter campaign.\n\nA couple of new Actions and Items inspired by other games from the publisher.\n\nRules: To add these cards to the game, shuffle them into corresponding markets while setting up the game. They can be bought during the game using the normal rules.\n\nIncludes 5 cards:\nGrowing Crystals, Hibernation, Flick of Determination, A Great Wall, Menhir",
"image": "https://cf.geekdo-images.com/yHRhOGjndW2ZiAr0kajCrw__original/img/41Q110pcbM3sQ8rZO6wjk0tBp1Q=/0x0/filters:format(png)/pic9122177.png",
"thumbnail": "https://cf.geekdo-images.com/yHRhOGjndW2ZiAr0kajCrw__small/img/-bs7koBkaPPACqWNQneRVctXzIc=/fit-in/200x150/filters:strip_icc()/pic9122177.png",
"min_players": 1,
"max_players": 4,
"best_player_counts": [
"1"
],
"playtime": 0,
"min_playtime": 0,
"max_playtime": 0,
"min_age": 0,
"designers": [],
"artists": [
"Ewa Labak",
"Michał Sałata"
],
"publishers": [
"Awaken Realms"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [],
"producers": [],
"series": [],
"rating": 7.40938,
"weight": 2.0,
"rank": null,
"users_owned": 425,
"version": {
"version_id": 597861,
"name": "English edition",
"year": 2022,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
],
"width_in": 3.54331,
"length_in": 2.44094,
"depth_in": 0.07874,
"weight_lb": 0.02205
},
"stored_in": null,
"dims": {
"width_in": 3.54331,
"length_in": 2.44094,
"depth_in": 0.07874,
"weight_lb": 0.02205,
"source": "version"
}
},
"291131:475388": {
"bgg_id": 291131,
"type": "boardgameexpansion",
"name": "Etherfields: Funeral Witch Campaign",
"year": 2022,
"description": "Story\n\nFuneral Witch is a very mysterious and dark figure, representing, among other things, sadness, pain and regret after a loss. She can be moody, chaotic, temperamental but still seeks solace - at each encounter her behaviour changes completely from the previous one. Will we discover the key to these changes? If so, will it be easier for us to prepare for the next encounter with her?\n\nFluctuation, but also cyclicality is the rhythm in which this campaign will be played. Will we drown in the depths of black sorrow, or will we find a ray of light that will lead us to a mysterious goal?\n\nIn this story we will be accompanied by drabness, darkness and ruins.\n\nGameplay\n\nIn this campaign, one of the main attractions, distinguishing it from the others, is a special additional 48-page Book of Scripts. Normally, in Etherfields, all adventures, scripts, conversations, scenes, etc. are placed directly on cards and tiles, and the amount of text is heavily balanced by graphics. Here, much more is pure text, and it is mainly used to conduct very extensive dialogues with the characters of this campaign.\n\nFeatured dream locations\n\nWe will climb the stairs and ride up in the old elevator to the highest floors of a crumbling, half-broken, block of flats.\n\nWe will come across a strange cemetery, located on the roof of a building.\n\nWe will visit the Museum of Death and the Great Cathedral.\n\nWe will struggle with the snow void.\n\nAnd we will take part in a game of hide and seek.\n\n—description from the publisher",
"image": "https://cf.geekdo-images.com/f7y3OmX7gPDntwh3MoILVQ__original/img/1difMUpNk3Plm2gRU9XoDDFsw4s=/0x0/filters:format(jpeg)/pic7021519.jpg",
"thumbnail": "https://cf.geekdo-images.com/f7y3OmX7gPDntwh3MoILVQ__small/img/hMaVrSJU8FkIC49I8qN4p1dQ4OU=/fit-in/200x150/filters:strip_icc()/pic7021519.jpg",
"min_players": 1,
"max_players": 4,
"best_player_counts": [
"2"
],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 14,
"designers": [
"Andrzej Betkiewicz",
"Michał Oracz"
],
"artists": [
"Piotr Gacek"
],
"publishers": [
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Adventure",
"Expansion for Base-game",
"Exploration",
"Fantasy",
"Miniatures"
],
"mechanics": [
"Cooperative Game",
"Deck, Bag, and Pool Building",
"Grid Movement",
"Modular Board",
"Role Playing",
"Storytelling"
],
"producers": [],
"series": [],
"rating": 8.36117,
"weight": 3.0,
"rank": null,
"users_owned": 4014,
"version": {
"version_id": 475388,
"name": "English edition",
"year": 2022,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
]
},
"stored_in": null,
"dims": {
"width_in": null,
"length_in": null,
"depth_in": null,
"weight_lb": null,
"source": "absent"
}
},
"309202:507805": {
"bgg_id": 309202,
"type": "boardgameexpansion",
"name": "Etherfields: Search Mini Deck",
"year": 2020,
"description": "The World of Dreams is vast and ever changing. In order to make your dreamy adventures easier, you have learned how to search for the tiniest of clues. The Search mini-deck will allow you to look deeper into your own dreams. But will you like what you will find?\n\nContents: 1 Rules card and new Search mini-deck consisting of 8 cards.\n\nAvailable as part of the 2020 Dice Tower Kickstarter Contenders Pack.",
"image": "https://cf.geekdo-images.com/-_IQS09ZDOz2zE2C0YQPcw__original/img/3Xhfx0WTSNDRr3bKqGH4lcgklME=/0x0/filters:format(jpeg)/pic5930951.jpg",
"thumbnail": "https://cf.geekdo-images.com/-_IQS09ZDOz2zE2C0YQPcw__small/img/TLrgJs2e_qxcJHLi1HQQuyY9peE=/fit-in/200x150/filters:strip_icc()/pic5930951.jpg",
"min_players": 1,
"max_players": 4,
"best_player_counts": [
"1"
],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 12,
"designers": [
"Michał Oracz"
],
"artists": [
"Piotr Gacek",
"Ewa Labak"
],
"publishers": [
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [
"Deck, Bag, and Pool Building"
],
"producers": [],
"series": [],
"rating": 7.57886,
"weight": 3.0,
"rank": null,
"users_owned": 612,
"version": {
"version_id": 507805,
"name": "English edition",
"year": 2020,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
],
"width_in": 3.50394,
"length_in": 2.48031,
"depth_in": 0.15748,
"weight_lb": 0.03968
},
"stored_in": null,
"dims": {
"width_in": 3.50394,
"length_in": 2.48031,
"depth_in": 0.15748,
"weight_lb": 0.03968,
"source": "version"
}
},
"381209:651213": {
"bgg_id": 381209,
"type": "boardgameexpansion",
"name": "Etherfields: Shifting Story Promo Deck",
"year": 2023,
"description": "Originally available as part of the Man vs. Meeple Season 4 Kickstarter.\n\nAdd the Shifting Story card to the Wisdom card holder. Place the rest of the cards from this deck as a separate deck in the box - referred to as the Shifting Deck.\n\nYour goal is to look for the displayed objects on the Old Dreams. If you see an objection on the board, you may read the associated card in the Shifting deck. If you find all the objects in the Dreamworld, you will be able to read the story's conclusion.\n\nContains 8 cards (63x87mm):\n7 Object cards (B-H)\n1 Rules/Reference card (A)",
"image": "https://cf.geekdo-images.com/_T6cKdo8PfKVt175Sjqh9w__original/img/LEbdjO9H10aW4tDOKNef3nHCukU=/0x0/filters:format(jpeg)/pic7370593.jpg",
"thumbnail": "https://cf.geekdo-images.com/_T6cKdo8PfKVt175Sjqh9w__small/img/L1aHDe7zKnLeVeYsdc6FUqDNMtY=/fit-in/200x150/filters:strip_icc()/pic7370593.jpg",
"min_players": 1,
"max_players": 4,
"best_player_counts": [],
"playtime": 0,
"min_playtime": 0,
"max_playtime": 0,
"min_age": 0,
"designers": [],
"artists": [
"Ewa Labak",
"Michał Sałata"
],
"publishers": [
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [],
"producers": [],
"series": [],
"rating": 7.0,
"weight": 0.0,
"rank": null,
"users_owned": 177,
"version": {
"version_id": 651213,
"name": "English edition",
"year": 2023,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
],
"width_in": 3.54331,
"length_in": 2.51969,
"depth_in": 0.07874,
"weight_lb": 0.03307
},
"stored_in": null,
"dims": {
"width_in": 3.54331,
"length_in": 2.51969,
"depth_in": 0.07874,
"weight_lb": 0.03307,
"source": "version"
}
},
"290840:474978": {
"bgg_id": 290840,
"type": "boardgameexpansion",
"name": "Etherfields: Sphinx Campaign",
"year": 2022,
"description": "The Sphinx is one of the oldest cults and a symbol of wisdom and mystery. Since ancient times, this magnificent creature was a guardian of secret knowledge. In this campaign you will be on a quest to discover a deeply hidden mystery that will bring a revolution to the world ruled under the Sphinx entity... What will it be...? What answers will you need to find...?\n\nIn this campaign, gameplay will be an endless maze that players will need to go through. From a components standpoint, you will have labyrinth tiles that you will be able to connect to one another in many different ways to try to pass through. However, if you end up at a dead end, you will need to go back to the start and try again.\n\nSecret knowledge will be very handy here as these tiles will have a lot of hidden mechanics of which you will have no knowledge at the start of the campaign. Only after unlocking some of the Sphinx mysteries, will you see a path in a place you previously thought was a dead end.",
"image": "https://cf.geekdo-images.com/XUWGdPg-EMto9ODlBnGE3w__original/img/FuJq6qLrxLqgelZyD0exXecPgMk=/0x0/filters:format(jpeg)/pic7021586.jpg",
"thumbnail": "https://cf.geekdo-images.com/XUWGdPg-EMto9ODlBnGE3w__small/img/DrAvXaXk1ougO-jbsulRG34e4qo=/fit-in/200x150/filters:strip_icc()/pic7021586.jpg",
"min_players": 1,
"max_players": 4,
"best_player_counts": [
"2"
],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 14,
"designers": [
"Michał Oracz"
],
"artists": [
"Piotr Gacek"
],
"publishers": [
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [
"Cooperative Game",
"Deck, Bag, and Pool Building"
],
"producers": [],
"series": [],
"rating": 8.00075,
"weight": 3.0,
"rank": null,
"users_owned": 3933,
"version": {
"version_id": 474978,
"name": "English edition",
"year": 2022,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
]
},
"stored_in": null,
"dims": {
"width_in": null,
"length_in": null,
"depth_in": null,
"weight_lb": null,
"source": "absent"
}
},
"386805:658656": {
"bgg_id": 386805,
"type": "boardgameexpansion",
"name": "Etherfields: Story Dice Expansion",
"year": 2024,
"description": "Introducing our latest innovation, the AR Vault: Story Dice. Elevate your gaming experience with fresh twists on storytelling, stunning dice, and unpredictable outcomes. Whether you're a tabletop RPG fanatic or a board game enthusiast, we've got you covered! Our Story Dice are 5e compatible, making them perfect for RPG players seeking something new and exciting. For Awaken Realms board game lovers, we offer dice expansions to bring an extra level of gameplay to your favorite games. Join us for the crowdfunding campaign and stay tuned for more exciting dice reveals. Get ready to roll with the AR Vault: Story Dice!\n\nLuck Die:\nWith this alternate Luck Die with a „high-risk, high-reward“ basis, players have a greater chance of rolling a critically good result, but be warned the stakes are high, and there is also a greater chance of rolling a critically negative result.\n\nMovement Die: \nWith this alternative Movement Die there is an increased chance, that the opponent move less. But beware this advantage comes at a cost. Using the alternate die will also buff the Entity, raising the stakes and adding a extra layer of tension to every move.\n\n—description from the publisher",
"image": "https://cf.geekdo-images.com/32-Zex3SY2iONm6YoHFOXg__original/img/xSAPlaWAm-eOBDA1mjyaNV1zaR4=/0x0/filters:format(jpeg)/pic8124079.jpg",
"thumbnail": "https://cf.geekdo-images.com/32-Zex3SY2iONm6YoHFOXg__small/img/aongQ-Yn_oftxbk14Zs2G9NW2aY=/fit-in/200x150/filters:strip_icc()/pic8124079.jpg",
"min_players": 1,
"max_players": 5,
"best_player_counts": [],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 12,
"designers": [
"Michał Oracz"
],
"artists": [],
"publishers": [
"Awaken Realms"
],
"categories": [
"Expansion for Base-game",
"Adventure",
"Exploration",
"Fantasy",
"Miniatures"
],
"mechanics": [
"Cooperative Game",
"Deck, Bag, and Pool Building",
"Dice Rolling",
"Grid Movement",
"Modular Board",
"Role Playing",
"Solo / Solitaire Game",
"Storytelling",
"Variable Player Powers"
],
"producers": [],
"series": [],
"rating": 8.175,
"weight": 0.0,
"rank": null,
"users_owned": 240,
"version": {
"version_id": 658656,
"name": "English edition",
"year": 2024,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
]
},
"stored_in": null,
"dims": {
"width_in": null,
"length_in": null,
"depth_in": null,
"weight_lb": null,
"source": "absent"
}
},
"351544:588185": {
"bgg_id": 351544,
"type": "boardgameexpansion",
"name": "Etherfields: Stretch Goals Harpy & She-Wolf Campaigns",
"year": 2022,
"description": "The Harpy Campaign adds 30+ hours of game play and exploration and the Harpy miniature.",
"image": "https://cf.geekdo-images.com/4mOB7GMg4bWVtcNui4QV2w__original/img/NM8npv-uifjP1EbjyzPojJQjrAE=/0x0/filters:format(jpeg)/pic7021596.jpg",
"thumbnail": "https://cf.geekdo-images.com/4mOB7GMg4bWVtcNui4QV2w__small/img/DmUWOsnpF6-II8ZpaOYOfRoBweU=/fit-in/200x150/filters:strip_icc()/pic7021596.jpg",
"min_players": 1,
"max_players": 4,
"best_player_counts": [
"2",
"3",
"4"
],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 12,
"designers": [
"Michał Oracz"
],
"artists": [
"Ewa Labak",
"Michał Sałata"
],
"publishers": [
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [],
"producers": [],
"series": [],
"rating": 8.14725,
"weight": 4.0,
"rank": null,
"users_owned": 4755,
"version": {
"version_id": 588185,
"name": "English edition",
"year": 2022,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
],
"width_in": 11.7,
"length_in": 11.7,
"depth_in": 2.8,
"weight_lb": 4.5327
},
"stored_in": null,
"dims": {
"width_in": 11.7,
"length_in": 11.7,
"depth_in": 2.8,
"weight_lb": 4.5327,
"source": "version"
}
},
"291126:475383": {
"bgg_id": 291126,
"type": "boardgameexpansion",
"name": "Etherfields: Thorn Knight",
"year": 2020,
"description": "The Thorn Knight is not a standard additional character, such as the 4 basic ones (Gambler, Specialist, Tough Guy, Free Spirit) or fifth with the 5-player expansion(Reaper).\n\nThe Thorn Knight is an alternative and very specific character available to ALL players at any time. During any given game, any player may decide to play the Thorn Knight as his normal in-game character.\n\nDuring that particular game, the player will not be using his standard character. Instead, his character's soul will enter this empty shell and guide this soulless Golem into its next adventure. The Ether acquired by this player will be an Ether belonging to the Thorn Knight. After the game, you will be able to spend that Eather to buy additional cards for the Thorn Knight (from the overall Action cards). The same goes for The Action cards won by this player during this game - they will enter the Thorn Knight's deck.\n\nPlayers can take turns playing the Thorn Knight through different games, jointly and gradually expanding his starting deck, using his starting special abilities, and finally turning him into a powerful, full of combos, common weapon for special challenges.\n\n—description from the publisher",
"image": "https://cf.geekdo-images.com/kMb10YpuAZJZrT7ijArCbQ__original/img/si0dJo9JHr9d7h3r1QMKU6Uc-y8=/0x0/filters:format(jpeg)/pic5055223.jpg",
"thumbnail": "https://cf.geekdo-images.com/kMb10YpuAZJZrT7ijArCbQ__small/img/iMH7IdUdahv6Rxx_jsc4rE5kxvs=/fit-in/200x150/filters:strip_icc()/pic5055223.jpg",
"min_players": 1,
"max_players": 4,
"best_player_counts": [],
"playtime": 180,
"min_playtime": 90,
"max_playtime": 180,
"min_age": 14,
"designers": [
"Michał Oracz"
],
"artists": [
"Piotr Gacek"
],
"publishers": [
"Awaken Realms",
"YOKA Games"
],
"categories": [
"Expansion for Base-game"
],
"mechanics": [
"Cooperative Game",
"Deck, Bag, and Pool Building"
],
"producers": [],
"series": [],
"rating": 7.33862,
"weight": 3.0,
"rank": null,
"users_owned": 2804,
"version": {
"version_id": 475383,
"name": "English edition",
"year": 2020,
"publishers": [
"Awaken Realms"
],
"languages": [
"English"
]
},
"stored_in": null,
"dims": {
"width_in": null,
"length_in": null,
"depth_in": null,
"weight_lb": null,
"source": "absent"
}
}
}
+510
View File
@@ -0,0 +1,510 @@
{
"104590:117256": {
"note": "",
"opening_id": "library-kallax-c1"
},
"104710:117685": {
"note": "",
"opening_id": "library-kallax-d1"
},
"116:28201": {
"note": "",
"opening_id": "library-kallax-c3"
},
"1197:78624": {
"note": "",
"opening_id": "library-kallax-c1"
},
"1219:488621": {
"note": "",
"opening_id": "library-kallax-c3"
},
"124548:165214": {
"note": "",
"opening_id": "library-kallax-d1"
},
"125675:167657": {
"note": "",
"opening_id": "library-kallax-c2"
},
"125921:476263": {
"note": "",
"opening_id": "library-kallax-c2"
},
"1260:156689": {
"note": "",
"opening_id": "library-kallax-b3"
},
"1269:29705": {
"note": "",
"opening_id": "library-kallax-a1"
},
"128554:207529": {
"note": "",
"opening_id": "library-kallax-d1"
},
"1339:34390": {
"note": "",
"opening_id": "library-kallax-c2"
},
"137909:216474": {
"note": "",
"opening_id": "library-kallax-a2"
},
"139443:202519": {
"note": "",
"opening_id": "library-kallax-b2"
},
"13:347121": {
"note": "",
"opening_id": "library-kallax-c2"
},
"143884:453051": {
"note": "",
"opening_id": "library-kallax-c3"
},
"14535:29075": {
"note": "",
"opening_id": "library-kallax-c4"
},
"147370:231664": {
"note": "",
"opening_id": "library-kallax-c4"
},
"150925:225659": {
"note": "",
"opening_id": "library-kallax-c3"
},
"153999:280845": {
"note": "",
"opening_id": "library-kallax-b4"
},
"156994:236280": {
"note": "",
"opening_id": "library-kallax-c4"
},
"157969:237864": {
"note": "",
"opening_id": "library-kallax-b1"
},
"160477:269392": {
"note": "",
"opening_id": "library-kallax-b4"
},
"161928:245124": {
"note": "",
"opening_id": "library-kallax-a2"
},
"163412:495998": {
"note": "",
"opening_id": "library-kallax-b4"
},
"164:28465": {
"note": "",
"opening_id": "library-kallax-b3"
},
"165986:277137": {
"note": "",
"opening_id": "library-kallax-c4"
},
"166109:253058": {
"note": "",
"opening_id": "library-kallax-b1"
},
"166976:410382": {
"note": "",
"opening_id": "library-kallax-b4"
},
"169124:451577": {
"note": "",
"opening_id": "library-kallax-b1"
},
"169784:260454": {
"note": "",
"opening_id": "library-kallax-c1"
},
"172225:296012": {
"note": "",
"opening_id": "library-kallax-c2"
},
"172503:265107": {
"note": "",
"opening_id": "library-kallax-b4"
},
"173057:266044": {
"note": "",
"opening_id": "library-kallax-b3"
},
"173634:266954": {
"note": "",
"opening_id": "library-kallax-b2"
},
"174430:520054": {
"note": "",
"opening_id": "library-kallax-a1"
},
"176:27673": {
"note": "",
"opening_id": "library-kallax-b3"
},
"177:24972": {
"note": "",
"opening_id": "library-kallax-d2"
},
"181304:536289": {
"note": "",
"opening_id": "library-kallax-b1"
},
"18755:284056": {
"note": "",
"opening_id": "library-kallax-c4"
},
"188614:779792": {
"note": "",
"opening_id": "library-kallax-a1"
},
"1927:28144": {
"note": "",
"opening_id": "library-kallax-c3"
},
"193485:300446": {
"note": "",
"opening_id": "library-kallax-c2"
},
"193621:300682": {
"note": "",
"opening_id": "library-kallax-b3"
},
"194655:668826": {
"note": "",
"opening_id": "library-kallax-c4"
},
"195314:525268": {
"note": "",
"opening_id": "library-kallax-c3"
},
"196105:304900": {
"note": "",
"opening_id": "library-kallax-b2"
},
"201248:313478": {
"note": "",
"opening_id": "library-kallax-c2"
},
"202982:316090": {
"note": "",
"opening_id": "library-kallax-b3"
},
"2058:28041": {
"note": "",
"opening_id": "library-kallax-d2"
},
"207830:344075": {
"note": "",
"opening_id": "library-kallax-c1"
},
"208428:325455": {
"note": "",
"opening_id": "library-kallax-c3"
},
"20866:40025": {
"note": "",
"opening_id": "library-kallax-a2"
},
"2197:42495": {
"note": "",
"opening_id": "library-kallax-b4"
},
"220199:369805": {
"note": "",
"opening_id": "library-kallax-a1"
},
"224:20727": {
"note": "",
"opening_id": "library-kallax-a1"
},
"226065:353389": {
"note": "",
"opening_id": "library-kallax-b1"
},
"231302:361880": {
"note": "",
"opening_id": "library-kallax-a2"
},
"2387:91428": {
"note": "",
"opening_id": "library-kallax-c3"
},
"240:24621": {
"note": "",
"opening_id": "library-kallax-d2"
},
"2452:288640": {
"note": "",
"opening_id": "library-kallax-b3"
},
"252153:404702": {
"note": "",
"opening_id": "library-kallax-c4"
},
"2524:219137": {
"note": "",
"opening_id": "library-kallax-d2"
},
"2529:98655": {
"note": "",
"opening_id": "library-kallax-d2"
},
"255984:701034": {
"note": "",
"opening_id": "library-kallax-d4"
},
"257:520364": {
"note": "",
"opening_id": "library-kallax-b3"
},
"258:20840": {
"note": "",
"opening_id": "library-kallax-b3"
},
"269564:439948": {
"note": "",
"opening_id": "library-kallax-b3"
},
"26:28607": {
"note": "",
"opening_id": "library-kallax-a1"
},
"273240:446069": {
"note": "",
"opening_id": "library-kallax-b2"
},
"27710:606657": {
"note": "",
"opening_id": "library-kallax-c2"
},
"280794:458300": {
"note": "",
"opening_id": "library-kallax-d3"
},
"283155:462049": {
"note": "",
"opening_id": "library-kallax-c1"
},
"283766:463140": {
"note": "",
"opening_id": "library-kallax-d4"
},
"291457:476060": {
"note": "",
"opening_id": "library-kallax-d3"
},
"291847:476820": {
"note": "",
"opening_id": "library-kallax-c3"
},
"2980:138013": {
"note": "",
"opening_id": "library-kallax-d1"
},
"3090:25782": {
"note": "",
"opening_id": "library-kallax-c2"
},
"31260:297589": {
"note": "",
"opening_id": "library-kallax-d3"
},
"312786:514050": {
"note": "",
"opening_id": "library-kallax-b3"
},
"3181:87931": {
"note": "",
"opening_id": "library-kallax-b3"
},
"320202:528008": {
"note": "",
"opening_id": "library-kallax-c2"
},
"324937:537230": {
"note": "",
"opening_id": "library-kallax-d1"
},
"329230:547518": {
"note": "",
"opening_id": "library-kallax-b3"
},
"332398:553061": {
"note": "",
"opening_id": "library-kallax-a2"
},
"334011:701315": {
"note": "",
"opening_id": "library-kallax-c1"
},
"334065:759453": {
"note": "",
"opening_id": "library-kallax-d1"
},
"340420:567183": {
"note": "",
"opening_id": "library-kallax-b2"
},
"352819:591147": {
"note": "",
"opening_id": "library-kallax-d1"
},
"35505:781669": {
"note": "",
"opening_id": "library-kallax-d1"
},
"358320:602661": {
"note": "",
"opening_id": "library-kallax-d4"
},
"3585:211154": {
"note": "",
"opening_id": "library-kallax-c4"
},
"358880:603694": {
"note": "",
"opening_id": "library-kallax-d3"
},
"362205:612448": {
"note": "",
"opening_id": "library-kallax-d4"
},
"380837:650545": {
"note": "",
"opening_id": "library-kallax-c1"
},
"389113:662372": {
"note": "",
"opening_id": "library-kallax-c4"
},
"393672:669782": {
"note": "",
"opening_id": "library-kallax-d3"
},
"39856:296529": {
"note": "",
"opening_id": "library-kallax-b1"
},
"40508:644326": {
"note": "",
"opening_id": "library-kallax-c4"
},
"40529:27692": {
"note": "",
"opening_id": "library-kallax-c2"
},
"40692:294178": {
"note": "",
"opening_id": "library-kallax-c4"
},
"419687:711400": {
"note": "",
"opening_id": "library-kallax-c3"
},
"42063:191736": {
"note": "",
"opening_id": "library-kallax-b3"
},
"4324:26859": {
"note": "",
"opening_id": "library-kallax-a2"
},
"432858:734890": {
"note": "",
"opening_id": "library-kallax-c1"
},
"43443:103068": {
"note": "",
"opening_id": "library-kallax-c1"
},
"442312:752105": {
"note": "",
"opening_id": "library-kallax-c2"
},
"452833:770724": {
"note": "",
"opening_id": "library-kallax-c1"
},
"453845:772660": {
"note": "",
"opening_id": "library-kallax-c3"
},
"483:48522": {
"note": "",
"opening_id": "library-kallax-d2"
},
"50381:134539": {
"note": "",
"opening_id": "library-kallax-b3"
},
"57215:295423": {
"note": "",
"opening_id": "library-kallax-b1"
},
"589:27352": {
"note": "",
"opening_id": "library-kallax-d1"
},
"65244:31208": {
"note": "",
"opening_id": "library-kallax-c3"
},
"71:24006": {
"note": "",
"opening_id": "library-kallax-d2"
},
"822:187468": {
"note": "",
"opening_id": "library-kallax-c1"
},
"9209:294188": {
"note": "",
"opening_id": "library-kallax-d1"
},
"95386:290572": {
"note": "",
"opening_id": "library-kallax-a1"
},
"9666:215137": {
"note": "",
"opening_id": "library-kallax-b3"
},
"97903:231971": {
"note": "",
"opening_id": "library-kallax-d1"
},
"local:cat crimes:IMG_4499.jpeg;IMG_4505.jpeg;shelf-20260803-215723.jpg": {
"note": "",
"opening_id": "library-kallax-c1"
},
"local:cat stax:IMG_4544.jpeg": {
"note": "",
"opening_id": "library-kallax-c2"
},
"local:elite sportz equipment table top curling:IMG_4526.jpeg": {
"note": "",
"opening_id": "library-kallax-c2"
},
"local:gravity maze:IMG_4510.jpeg": {
"note": "",
"opening_id": "library-kallax-c3"
},
"local:laser maze beam bending logic game:IMG_4516.jpeg": {
"note": "",
"opening_id": "library-kallax-c3"
},
"local:ouija:IMG_4529.jpeg": {
"note": "",
"opening_id": "library-kallax-c4"
},
"local:super mario checkers collector s edition:IMG_4508.jpeg": {
"note": "",
"opening_id": "library-kallax-c4"
}
}
+9
View File
@@ -150,3 +150,12 @@ The Red Dragon Inn: Allies Jin vs Greppa,442637,The Red Dragon Inn: Allies
The Red Dragon Inn: Allies Melvyn vs. Marah,403503,The Red Dragon Inn: Allies Melvyn vs. Marah,2024,boardgameexpansion,approved,712612,English Kickstarter edition,version_approved,[],"[{""version_id"": 685052, ""name"": ""English edition"", ""year"": 2024, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 712612, ""name"": ""English Kickstarter edition"", ""year"": 2024, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 2}]",,,,173634
The Red Dragon Inn: Allies Pooky,125472,The Red Dragon Inn: Allies Pooky,2012,boardgameexpansion,approved,629302,English fourth edition,version_approved,[],"[{""version_id"": 167237, ""name"": ""English first edition"", ""year"": 2012, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 401403, ""name"": ""English second edition"", ""year"": 2015, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 580866, ""name"": ""English third edition"", ""year"": 2018, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 629302, ""name"": ""English fourth edition"", ""year"": 2021, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}, {""version_id"": 781893, ""name"": ""English fifth edition"", ""year"": 2025, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}]",,,,173634
The Red Dragon Inn: Allies Wrench,168857,The Red Dragon Inn: Allies Wrench,2015,boardgameexpansion,approved,657857,English third edition,version_approved,[],"[{""version_id"": 258498, ""name"": ""English first edition"", ""year"": 2015, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 657857, ""name"": ""English third edition"", ""year"": 2020, ""publishers"": [""SlugFest Games""], ""languages"": [""English""], ""score"": 0}]",,,,173634
Etherfields: 5th Player Expansion,291130,Etherfields: 5th Player Expansion,2020,boardgameexpansion,approved,475387,English edition,version_approved,[],"[{""version_id"": 475387, ""name"": ""English edition"", ""year"": 2020, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 758501, ""name"": ""Chinese edition"", ""year"": 2021, ""publishers"": [""YOKA Games""], ""languages"": [""Chinese""], ""score"": 0}, {""version_id"": 518873, ""name"": ""French edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""French""], ""score"": 0}, {""version_id"": 518875, ""name"": ""German edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""German""], ""score"": 0}, {""version_id"": 518876, ""name"": ""Italian edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""Italian""], ""score"": 0}, {""version_id"": 518878, ""name"": ""Polish edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 0}, {""version_id"": 518879, ""name"": ""Spanish edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 0}, {""version_id"": 762887, ""name"": ""Czech edition"", ""year"": 2024, ""publishers"": [""Albi""], ""languages"": [""Czech""], ""score"": 0}]",,,,
Etherfields: Awakened Items & Actions,355873,Etherfields: Awakened Items & Actions,2022,boardgameexpansion,approved,597861,English edition,version_approved,[],"[{""version_id"": 597861, ""name"": ""English edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}]",,,,
Etherfields: Funeral Witch Campaign,291131,Etherfields: Funeral Witch Campaign,2022,boardgameexpansion,approved,475388,English edition,version_approved,[],"[{""version_id"": 722494, ""name"": ""Chinese edition"", ""year"": 2022, ""publishers"": [""Awaken Realms"", ""YOKA Games""], ""languages"": [""Chinese""], ""score"": 2}, {""version_id"": 475388, ""name"": ""English edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 518859, ""name"": ""French edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""French""], ""score"": 2}, {""version_id"": 518861, ""name"": ""German edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 518863, ""name"": ""Italian edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Italian""], ""score"": 2}, {""version_id"": 518865, ""name"": ""Polish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 2}, {""version_id"": 518866, ""name"": ""Spanish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 2}]",,,,
Etherfields: Search Mini Deck,309202,Etherfields: Search Mini Deck,2020,boardgameexpansion,approved,507805,English edition,version_approved,[],"[{""version_id"": 507805, ""name"": ""English edition"", ""year"": 2020, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 758508, ""name"": ""Chinese edition"", ""year"": 2023, ""publishers"": [""YOKA Games""], ""languages"": [""Chinese""], ""score"": 0}]",,,,
Etherfields: Shifting Story Promo Deck,381209,Etherfields: Shifting Story Promo Deck,2023,boardgameexpansion,approved,651213,English edition,version_approved,[],"[{""version_id"": 758494, ""name"": ""Chinese edition"", ""year"": 2023, ""publishers"": [""YOKA Games""], ""languages"": [""Chinese""], ""score"": 2}, {""version_id"": 651213, ""name"": ""English edition"", ""year"": 2023, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}]",,,,
Etherfields: Sphinx Campaign,290840,Etherfields: Sphinx Campaign,2022,boardgameexpansion,approved,474978,English edition,version_approved,[],"[{""version_id"": 722495, ""name"": ""Chinese edition"", ""year"": 2022, ""publishers"": [""Awaken Realms"", ""YOKA Games""], ""languages"": [""Chinese""], ""score"": 2}, {""version_id"": 474978, ""name"": ""English edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 518849, ""name"": ""French edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""French""], ""score"": 2}, {""version_id"": 518850, ""name"": ""German edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 518851, ""name"": ""Italian edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Italian""], ""score"": 2}, {""version_id"": 518853, ""name"": ""Spanish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 2}, {""version_id"": 518852, ""name"": ""Polish edition"", ""year"": 2023, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 0}]",,,,
Etherfields: Story Dice Expansion,386805,Etherfields: Story Dice Expansion,2024,boardgameexpansion,approved,658656,English edition,version_approved,[],"[{""version_id"": 658656, ""name"": ""English edition"", ""year"": 2024, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 658660, ""name"": ""French edition"", ""year"": 2024, ""publishers"": [""Awaken Realms""], ""languages"": [""French""], ""score"": 2}, {""version_id"": 658658, ""name"": ""German edition"", ""year"": 2024, ""publishers"": [""Awaken Realms""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 658661, ""name"": ""Italian edition"", ""year"": 2024, ""publishers"": [""Awaken Realms""], ""languages"": [""Italian""], ""score"": 2}, {""version_id"": 658659, ""name"": ""Polish edition"", ""year"": 2024, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 2}, {""version_id"": 658657, ""name"": ""Spanish edition"", ""year"": 2024, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 2}]",,,,
Etherfields: Stretch Goals Harpy & She-Wolf Campaigns,351544,Etherfields: Stretch Goals Harpy & She-Wolf Campaigns,2022,boardgameexpansion,approved,588185,English edition,version_approved,[],"[{""version_id"": 722493, ""name"": ""Chinese edition"", ""year"": 2022, ""publishers"": [""Awaken Realms"", ""YOKA Games""], ""languages"": [""Chinese""], ""score"": 2}, {""version_id"": 588185, ""name"": ""English edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 669930, ""name"": ""Spanish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 2}, {""version_id"": 640104, ""name"": ""French edition"", ""year"": 2023, ""publishers"": [""Awaken Realms""], ""languages"": [""French""], ""score"": 0}, {""version_id"": 653469, ""name"": ""German edition"", ""year"": 2023, ""publishers"": [""Awaken Realms""], ""languages"": [""German""], ""score"": 0}, {""version_id"": 647233, ""name"": ""Italian edition"", ""year"": 2023, ""publishers"": [""Awaken Realms""], ""languages"": [""Italian""], ""score"": 0}, {""version_id"": 653623, ""name"": ""Polish edition"", ""year"": 2023, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 0}]",,,,
Etherfields: Thorn Knight,291126,Etherfields: Thorn Knight,2020,boardgameexpansion,approved,475383,English edition,version_approved,[],"[{""version_id"": 475383, ""name"": ""English edition"", ""year"": 2020, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 2}, {""version_id"": 758493, ""name"": ""Chinese edition"", ""year"": 2021, ""publishers"": [""YOKA Games""], ""languages"": [""Chinese""], ""score"": 0}, {""version_id"": 518880, ""name"": ""French edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""French""], ""score"": 0}, {""version_id"": 518881, ""name"": ""German edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""German""], ""score"": 0}, {""version_id"": 518882, ""name"": ""Italian edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""Italian""], ""score"": 0}, {""version_id"": 518884, ""name"": ""Polish edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 0}, {""version_id"": 518886, ""name"": ""Spanish edition"", ""year"": 2021, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 0}]",,,,
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 stored_in
150 The Red Dragon Inn: Allies – Melvyn vs. Marah 403503 The Red Dragon Inn: Allies – Melvyn vs. Marah 2024 boardgameexpansion approved 712612 English Kickstarter edition version_approved [] [{"version_id": 685052, "name": "English edition", "year": 2024, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 2}, {"version_id": 712612, "name": "English Kickstarter edition", "year": 2024, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 2}] 173634
151 The Red Dragon Inn: Allies – Pooky 125472 The Red Dragon Inn: Allies – Pooky 2012 boardgameexpansion approved 629302 English fourth edition version_approved [] [{"version_id": 167237, "name": "English first edition", "year": 2012, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 2}, {"version_id": 401403, "name": "English second edition", "year": 2015, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 0}, {"version_id": 580866, "name": "English third edition", "year": 2018, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 0}, {"version_id": 629302, "name": "English fourth edition", "year": 2021, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 0}, {"version_id": 781893, "name": "English fifth edition", "year": 2025, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 0}] 173634
152 The Red Dragon Inn: Allies – Wrench 168857 The Red Dragon Inn: Allies – Wrench 2015 boardgameexpansion approved 657857 English third edition version_approved [] [{"version_id": 258498, "name": "English first edition", "year": 2015, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 2}, {"version_id": 657857, "name": "English third edition", "year": 2020, "publishers": ["SlugFest Games"], "languages": ["English"], "score": 0}] 173634
153 Etherfields: 5th Player Expansion 291130 Etherfields: 5th Player Expansion 2020 boardgameexpansion approved 475387 English edition version_approved [] [{"version_id": 475387, "name": "English edition", "year": 2020, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 758501, "name": "Chinese edition", "year": 2021, "publishers": ["YOKA Games"], "languages": ["Chinese"], "score": 0}, {"version_id": 518873, "name": "French edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["French"], "score": 0}, {"version_id": 518875, "name": "German edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["German"], "score": 0}, {"version_id": 518876, "name": "Italian edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["Italian"], "score": 0}, {"version_id": 518878, "name": "Polish edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 0}, {"version_id": 518879, "name": "Spanish edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 0}, {"version_id": 762887, "name": "Czech edition", "year": 2024, "publishers": ["Albi"], "languages": ["Czech"], "score": 0}]
154 Etherfields: Awakened Items & Actions 355873 Etherfields: Awakened Items & Actions 2022 boardgameexpansion approved 597861 English edition version_approved [] [{"version_id": 597861, "name": "English edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}]
155 Etherfields: Funeral Witch Campaign 291131 Etherfields: Funeral Witch Campaign 2022 boardgameexpansion approved 475388 English edition version_approved [] [{"version_id": 722494, "name": "Chinese edition", "year": 2022, "publishers": ["Awaken Realms", "YOKA Games"], "languages": ["Chinese"], "score": 2}, {"version_id": 475388, "name": "English edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 518859, "name": "French edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["French"], "score": 2}, {"version_id": 518861, "name": "German edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["German"], "score": 2}, {"version_id": 518863, "name": "Italian edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Italian"], "score": 2}, {"version_id": 518865, "name": "Polish edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 2}, {"version_id": 518866, "name": "Spanish edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 2}]
156 Etherfields: Search Mini Deck 309202 Etherfields: Search Mini Deck 2020 boardgameexpansion approved 507805 English edition version_approved [] [{"version_id": 507805, "name": "English edition", "year": 2020, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 758508, "name": "Chinese edition", "year": 2023, "publishers": ["YOKA Games"], "languages": ["Chinese"], "score": 0}]
157 Etherfields: Shifting Story Promo Deck 381209 Etherfields: Shifting Story Promo Deck 2023 boardgameexpansion approved 651213 English edition version_approved [] [{"version_id": 758494, "name": "Chinese edition", "year": 2023, "publishers": ["YOKA Games"], "languages": ["Chinese"], "score": 2}, {"version_id": 651213, "name": "English edition", "year": 2023, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}]
158 Etherfields: Sphinx Campaign 290840 Etherfields: Sphinx Campaign 2022 boardgameexpansion approved 474978 English edition version_approved [] [{"version_id": 722495, "name": "Chinese edition", "year": 2022, "publishers": ["Awaken Realms", "YOKA Games"], "languages": ["Chinese"], "score": 2}, {"version_id": 474978, "name": "English edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 518849, "name": "French edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["French"], "score": 2}, {"version_id": 518850, "name": "German edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["German"], "score": 2}, {"version_id": 518851, "name": "Italian edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Italian"], "score": 2}, {"version_id": 518853, "name": "Spanish edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 2}, {"version_id": 518852, "name": "Polish edition", "year": 2023, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 0}]
159 Etherfields: Story Dice Expansion 386805 Etherfields: Story Dice Expansion 2024 boardgameexpansion approved 658656 English edition version_approved [] [{"version_id": 658656, "name": "English edition", "year": 2024, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 658660, "name": "French edition", "year": 2024, "publishers": ["Awaken Realms"], "languages": ["French"], "score": 2}, {"version_id": 658658, "name": "German edition", "year": 2024, "publishers": ["Awaken Realms"], "languages": ["German"], "score": 2}, {"version_id": 658661, "name": "Italian edition", "year": 2024, "publishers": ["Awaken Realms"], "languages": ["Italian"], "score": 2}, {"version_id": 658659, "name": "Polish edition", "year": 2024, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 2}, {"version_id": 658657, "name": "Spanish edition", "year": 2024, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 2}]
160 Etherfields: Stretch Goals – Harpy & She-Wolf Campaigns 351544 Etherfields: Stretch Goals – Harpy & She-Wolf Campaigns 2022 boardgameexpansion approved 588185 English edition version_approved [] [{"version_id": 722493, "name": "Chinese edition", "year": 2022, "publishers": ["Awaken Realms", "YOKA Games"], "languages": ["Chinese"], "score": 2}, {"version_id": 588185, "name": "English edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 669930, "name": "Spanish edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 2}, {"version_id": 640104, "name": "French edition", "year": 2023, "publishers": ["Awaken Realms"], "languages": ["French"], "score": 0}, {"version_id": 653469, "name": "German edition", "year": 2023, "publishers": ["Awaken Realms"], "languages": ["German"], "score": 0}, {"version_id": 647233, "name": "Italian edition", "year": 2023, "publishers": ["Awaken Realms"], "languages": ["Italian"], "score": 0}, {"version_id": 653623, "name": "Polish edition", "year": 2023, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 0}]
161 Etherfields: Thorn Knight 291126 Etherfields: Thorn Knight 2020 boardgameexpansion approved 475383 English edition version_approved [] [{"version_id": 475383, "name": "English edition", "year": 2020, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 2}, {"version_id": 758493, "name": "Chinese edition", "year": 2021, "publishers": ["YOKA Games"], "languages": ["Chinese"], "score": 0}, {"version_id": 518880, "name": "French edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["French"], "score": 0}, {"version_id": 518881, "name": "German edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["German"], "score": 0}, {"version_id": 518882, "name": "Italian edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["Italian"], "score": 0}, {"version_id": 518884, "name": "Polish edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 0}, {"version_id": 518886, "name": "Spanish edition", "year": 2021, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 0}]
+90
View File
@@ -178,5 +178,95 @@
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: 5th Player Expansion",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2020,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Awakened Items & Actions",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Funeral Witch Campaign",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Search Mini Deck",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2020,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Shifting Story Promo Deck",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2023,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Sphinx Campaign",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Story Dice Expansion",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2024,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Stretch Goals Harpy & She-Wolf Campaigns",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": []
},
{
"title_raw": "Etherfields: Thorn Knight",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2020,
"language_hint": "",
"art_notes": "",
"source_photos": []
}
]
+99
View File
@@ -1944,5 +1944,104 @@
"art_notes": "",
"source_photos": [],
"title_normalized": "red dragon inn allies wrench"
},
{
"title_raw": "Etherfields: 5th Player Expansion",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2020,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields 5th player expansion"
},
{
"title_raw": "Etherfields: Awakened Items & Actions",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields awakened items and actions"
},
{
"title_raw": "Etherfields: Funeral Witch Campaign",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields funeral witch campaign"
},
{
"title_raw": "Etherfields: Search Mini Deck",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2020,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields search mini deck"
},
{
"title_raw": "Etherfields: Shifting Story Promo Deck",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2023,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields shifting story promo deck"
},
{
"title_raw": "Etherfields: Sphinx Campaign",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields sphinx campaign"
},
{
"title_raw": "Etherfields: Story Dice Expansion",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2024,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields story dice expansion"
},
{
"title_raw": "Etherfields: Stretch Goals Harpy & She-Wolf Campaigns",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2022,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields stretch goals harpy and she wolf campaigns"
},
{
"title_raw": "Etherfields: Thorn Knight",
"confidence": "high",
"publisher_hint": "",
"edition_hint": "",
"year_hint": 2020,
"language_hint": "",
"art_notes": "",
"source_photos": [],
"title_normalized": "etherfields thorn knight"
}
]
+9
View File
@@ -194,3 +194,12 @@ add,442637,,The Red Dragon Inn: Allies Jin vs Greppa,752778,,added,2026-08-0
add,403503,,The Red Dragon Inn: Allies Melvyn vs. Marah,712612,,added,2026-08-09T16:25:40+00:00,
add,125472,,The Red Dragon Inn: Allies Pooky,629302,,added,2026-08-09T16:25:47+00:00,
add,168857,,The Red Dragon Inn: Allies Wrench,657857,,added,2026-08-09T16:25:54+00:00,
add,291130,,Etherfields: 5th Player Expansion,475387,,added,2026-08-09T18:47:14+00:00,
add,355873,,Etherfields: Awakened Items & Actions,597861,,added,2026-08-09T18:47:22+00:00,
add,291131,,Etherfields: Funeral Witch Campaign,475388,,added,2026-08-09T18:47:30+00:00,
add,309202,,Etherfields: Search Mini Deck,507805,,added,2026-08-09T18:47:37+00:00,
add,381209,,Etherfields: Shifting Story Promo Deck,651213,,added,2026-08-09T18:47:46+00:00,
add,290840,,Etherfields: Sphinx Campaign,474978,,added,2026-08-09T18:47:54+00:00,
add,386805,,Etherfields: Story Dice Expansion,658656,,added,2026-08-09T18:48:00+00:00,
add,351544,,Etherfields: Stretch Goals Harpy & She-Wolf Campaigns,588185,,added,2026-08-09T18:48:08+00:00,
add,291126,,Etherfields: Thorn Knight,475383,,added,2026-08-09T18:48:17+00:00,
1 action bgg_id collid name version_id second_copy status timestamp error
194 add 403503 The Red Dragon Inn: Allies – Melvyn vs. Marah 712612 added 2026-08-09T16:25:40+00:00
195 add 125472 The Red Dragon Inn: Allies – Pooky 629302 added 2026-08-09T16:25:47+00:00
196 add 168857 The Red Dragon Inn: Allies – Wrench 657857 added 2026-08-09T16:25:54+00:00
197 add 291130 Etherfields: 5th Player Expansion 475387 added 2026-08-09T18:47:14+00:00
198 add 355873 Etherfields: Awakened Items & Actions 597861 added 2026-08-09T18:47:22+00:00
199 add 291131 Etherfields: Funeral Witch Campaign 475388 added 2026-08-09T18:47:30+00:00
200 add 309202 Etherfields: Search Mini Deck 507805 added 2026-08-09T18:47:37+00:00
201 add 381209 Etherfields: Shifting Story Promo Deck 651213 added 2026-08-09T18:47:46+00:00
202 add 290840 Etherfields: Sphinx Campaign 474978 added 2026-08-09T18:47:54+00:00
203 add 386805 Etherfields: Story Dice Expansion 658656 added 2026-08-09T18:48:00+00:00
204 add 351544 Etherfields: Stretch Goals – Harpy & She-Wolf Campaigns 588185 added 2026-08-09T18:48:08+00:00
205 add 291126 Etherfields: Thorn Knight 475383 added 2026-08-09T18:48:17+00:00
+131
View File
@@ -0,0 +1,131 @@
name,opening
Gloomhaven,A1
History of the World,A1
Age of Renaissance,A1
Simon's Cat Card Game,A1
Skip-Bo,A1
Snappy Dressers,A1
Tem-Purr-A,A1
Everdell: The Complete Collection,A2
Bugs in the Kitchen,A2
Risk: The Lord of the Rings,A2
The Cat Game,A2
The Rocky Horror Trivia Game,A2
Utter Nonsense,A2
Dixit,B1
Flick 'em Up!,B1
Googly Eyes,B1
Meteor,B1
Mysterium,B1
Sheriff of Nottingham,B1
Sheriff of Nottingham: Merry Men,B1
Speechless,B2
Superfight,B2
The Red Dragon Inn,B2
The Red Dragon Inn 5: The Character Trove,B2
The Red Dragon Inn Smorgasbox,B2
Throw Throw Avocado,B2
"Before I Kill You, Mister Spy...",B3
Bluffaneer,B3
Captain Marvel: Secret Skrulls,B3
Cards Against Humanity,B3
Code Master,B3
Farkle,B3
Fluxx,B3
Give Me the Brain!,B3
Go Fish Yourself,B3
Greed Quest,B3
Jenga,B3
Joking Hazard,B3
Kill Doctor Lucky,B3
Poetry for Neanderthals,B3
Rook,B3
Scrawl,B3
"...and then, we held hands.",B4
Consentacle,B4
Kensington,B4
Mage Wars Academy,B4
Onitama,B4
Patchwork,B4
5-Minute Dungeon,C1
A Gentle Rain,C1
Artistry,C1
Artistry: Delightful Doorways Mini Expansion,C1
Blink,C1
Botany,C1
Botany: Perilous Perfumes,C1
Botany: Tantalizing Trees,C1
Calico,C1
Carcassonne,C1
Castle Panic,C1
Castle Panic: The Dark Titan,C1
Castle Panic: The Wizard's Tower,C1
CAT CRIMES,C1
CAT STAX,C2
Catan,C2
Catan Dice Game,C2
Catan: Junior,C2
Cosmic Encounter,C2
Dastardly Dirigibles,C2
Deadly Doodles,C2
Doctor Who: The Card Game,C2
Doctor Who: The Game of Time & Space,C2
Dragon Land,C2
Dungeon!,C2
Elite Sportz Equipment Table Top Curling,C2
Evolution: The Beginning,C2
Exploding Kittens,C2
Forbidden Island,C3
GRAVITY MAZE,C3
Guillotine,C3
Herbaceous,C3
Hold Your Breath!,C3
Huggermugger,C3
Labyrinth,C3
LASER MAZE BEAM-BENDING LOGIC GAME,C3
Machi Koro,C3
Mantis Falls,C3
Munchkin,C3
Munchkin Big Box,C3
Murder at the Manor,C3
"No Thank You, Evil!",C3
Ouija,C4
Pie Face!,C4
Rivals,C4
Robot Turtles,C4
Royals,C4
Santorini,C4
Scrabble Slam!,C4
Skylanders Portal Master,C4
Small World,C4
Sorcerer: The Game of Magical Conflict,C4
SPANC: Space Pirate Amazon Ninja Catgirls,C4
Super Mario Checkers Collector's Edition,C4
Tang Garden,C4
Terror in Meeple City,D1
The Doom That Came to Atlantic City,D1
The Office Trivia Game,D1
The Simpsons Trivia Game,D1
Ticket to Ride,D1
Verdant,D1
Völuspá,D1
Walk the Plank!,D1
Wiz-War,D1
Wiz-War (Eighth Edition),D1
Wiz-War (9th Edition),D1
Advanced Civilization,D2
Britannia,D2
Civilization,D2
Civilization: West Extension Map,D2
Diplomacy,D2
Flat Top,D2
StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century,D2
Agricola,D3
Etherfields,D3
Etherfields: Kittenburg Expansion,D3
Gloomhaven: Buttons & Bugs,D3
Gloomhaven: Jaws of the Lion,D3
Sleeping Gods,D4
Sleeping Gods: Distant Skies,D4
Sleeping Gods: Primeval Peril,D4
Sleeping Gods: Tides of Ruin,D4
1 name opening
2 Gloomhaven A1
3 History of the World A1
4 Age of Renaissance A1
5 Simon's Cat Card Game A1
6 Skip-Bo A1
7 Snappy Dressers A1
8 Tem-Purr-A A1
9 Everdell: The Complete Collection A2
10 Bugs in the Kitchen A2
11 Risk: The Lord of the Rings A2
12 The Cat Game A2
13 The Rocky Horror Trivia Game A2
14 Utter Nonsense A2
15 Dixit B1
16 Flick 'em Up! B1
17 Googly Eyes B1
18 Meteor B1
19 Mysterium B1
20 Sheriff of Nottingham B1
21 Sheriff of Nottingham: Merry Men B1
22 Speechless B2
23 Superfight B2
24 The Red Dragon Inn B2
25 The Red Dragon Inn 5: The Character Trove B2
26 The Red Dragon Inn Smorgasbox B2
27 Throw Throw Avocado B2
28 Before I Kill You, Mister Spy... B3
29 Bluffaneer B3
30 Captain Marvel: Secret Skrulls B3
31 Cards Against Humanity B3
32 Code Master B3
33 Farkle B3
34 Fluxx B3
35 Give Me the Brain! B3
36 Go Fish Yourself B3
37 Greed Quest B3
38 Jenga B3
39 Joking Hazard B3
40 Kill Doctor Lucky B3
41 Poetry for Neanderthals B3
42 Rook B3
43 Scrawl B3
44 ...and then, we held hands. B4
45 Consentacle B4
46 Kensington B4
47 Mage Wars Academy B4
48 Onitama B4
49 Patchwork B4
50 5-Minute Dungeon C1
51 A Gentle Rain C1
52 Artistry C1
53 Artistry: Delightful Doorways Mini Expansion C1
54 Blink C1
55 Botany C1
56 Botany: Perilous Perfumes C1
57 Botany: Tantalizing Trees C1
58 Calico C1
59 Carcassonne C1
60 Castle Panic C1
61 Castle Panic: The Dark Titan C1
62 Castle Panic: The Wizard's Tower C1
63 CAT CRIMES C1
64 CAT STAX C2
65 Catan C2
66 Catan Dice Game C2
67 Catan: Junior C2
68 Cosmic Encounter C2
69 Dastardly Dirigibles C2
70 Deadly Doodles C2
71 Doctor Who: The Card Game C2
72 Doctor Who: The Game of Time & Space C2
73 Dragon Land C2
74 Dungeon! C2
75 Elite Sportz Equipment Table Top Curling C2
76 Evolution: The Beginning C2
77 Exploding Kittens C2
78 Forbidden Island C3
79 GRAVITY MAZE C3
80 Guillotine C3
81 Herbaceous C3
82 Hold Your Breath! C3
83 Huggermugger C3
84 Labyrinth C3
85 LASER MAZE BEAM-BENDING LOGIC GAME C3
86 Machi Koro C3
87 Mantis Falls C3
88 Munchkin C3
89 Munchkin Big Box C3
90 Murder at the Manor C3
91 No Thank You, Evil! C3
92 Ouija C4
93 Pie Face! C4
94 Rivals C4
95 Robot Turtles C4
96 Royals C4
97 Santorini C4
98 Scrabble Slam! C4
99 Skylanders Portal Master C4
100 Small World C4
101 Sorcerer: The Game of Magical Conflict C4
102 SPANC: Space Pirate Amazon Ninja Catgirls C4
103 Super Mario Checkers Collector's Edition C4
104 Tang Garden C4
105 Terror in Meeple City D1
106 The Doom That Came to Atlantic City D1
107 The Office Trivia Game D1
108 The Simpsons Trivia Game D1
109 Ticket to Ride D1
110 Verdant D1
111 Völuspá D1
112 Walk the Plank! D1
113 Wiz-War D1
114 Wiz-War (Eighth Edition) D1
115 Wiz-War (9th Edition) D1
116 Advanced Civilization D2
117 Britannia D2
118 Civilization D2
119 Civilization: West Extension Map D2
120 Diplomacy D2
121 Flat Top D2
122 StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century D2
123 Agricola D3
124 Etherfields D3
125 Etherfields: Kittenburg Expansion D3
126 Gloomhaven: Buttons & Bugs D3
127 Gloomhaven: Jaws of the Lion D3
128 Sleeping Gods D4
129 Sleeping Gods: Distant Skies D4
130 Sleeping Gods: Primeval Peril D4
131 Sleeping Gods: Tides of Ruin D4
+13 -3
View File
@@ -145,14 +145,24 @@ chains (minis in an insert in a big box live where the big box does). On a
phone over `--lan`, the opening view docks as a bottom sheet — the
moving-day loop is search, tap, done.
When you publish with `bggpipe export`, the shelves come too: game pages
say where each box lives, containers list their contents, and a shelves
page draws the wall as it physically stands, every opening linking its
residents. Publishing your shelf layout is a choice — `--no-shelves`
keeps the whole layer out of the export.
Bulk-load a reviewed plan with `bggpipe shelve --import plan.csv` (columns
`name,opening`, accepting opening ids or labels) — rejects are reported
(unknown name, ambiguous copies, unknown opening, doesn't fit, lives
inside another box), never guessed, and re-imports preserve hand-entered
notes. `bggpipe dims` reports per-opening capacity, overfull warnings, and
misfits against your real furniture. Fill bars measure stacked thickness
against interior height (boxes lying flat); unmeasured boxes are counted
and shown, never silently assumed to fit.
misfits against your real furniture. Capacity uses a **two-lane packing
model**: each opening holds a flat stack (thinnest axes summing against
interior height, claiming the widest flat box's width) and a standing lane
beside it (boxes on edge against the remaining width) — the largest boxes
lie flat, smaller ones stand, and you can flip any box's lane in the
opening's view. An opening warns only when *no* packing fits. Unmeasured
boxes are counted and shown, never silently assumed to fit.
## Shelf-space planning
+9 -1
View File
@@ -229,13 +229,21 @@ def export(
str,
typer.Option("--title", help="Site title shown on every page"),
] = "My Game Library",
no_shelves: Annotated[
bool,
typer.Option(
"--no-shelves",
help="Omit the shelves page and locations (publishing your "
"shelf layout is a choice)",
),
] = False,
config: ConfigOpt = None,
) -> None:
"""Export the library as self-contained static pages (for a blog etc.)."""
from bggpipe.export import run_export
cfg = load_config(config)
summary = run_export(cfg, out, title=title)
summary = run_export(cfg, out, title=title, include_shelves=not no_shelves)
if summary["failures"]:
# scripted `export && rsync` must not publish an incomplete site
raise typer.Exit(code=1)
+11 -5
View File
@@ -118,16 +118,22 @@ def run_dims_report(cfg: Config) -> dict:
f" {opening['unit']} · {opening['label']}"
f" ({dims_note}): {report['games']} game(s)"
)
if report["stacked_in"]:
line += f', {report["stacked_in"]:g}" stacked'
if report["flat_in"]:
line += f', {report["flat_in"]:g}" flat stack'
if report["standing_in"]:
line += f', {report["standing_in"]:g}" standing'
if report["unmeasured"]:
line += f", {report['unmeasured']} unmeasured"
if report["overfull"]:
line += "OVERFULL"
line += "NO PACKING FITS"
typer.echo(line)
for name in report["misfits"]:
for name in report["laneless"]:
misfit_names.append(name)
typer.echo(f"doesn't fit: {name}")
typer.echo(f"no lane can take: {name}")
for name in report["misfits"]:
if name not in misfit_names:
misfit_names.append(name)
typer.echo(f" ⚠ doesn't fit this opening at all: {name}")
homeless = [
entry.get("name") or "?"
for key, entry in games.items()
+166 -3
View File
@@ -26,6 +26,12 @@ import typer
from bggpipe.config import Config
from bggpipe.fsio import atomic_write_bytes, atomic_write_text
from bggpipe.shelves import (
effective_location,
load_furniture,
load_locations,
opening_index,
)
# records which slugs THIS export owns, and their key assignments: slug
# stability across runs (a published URL never changes or is stolen) and
@@ -61,8 +67,9 @@ def _slugs(games: dict[str, dict], prior: dict[str, str]) -> dict[str, str]:
rename), and a new key can never steal one — collisions take -2, -3...
in sorted-key order."""
out: dict[str, str] = {k: v for k, v in prior.items() if k in games}
# "art" is the asset directory: a game named Art must not move in
taken: set[str] = set(out.values()) | {"art"}
# "art" is the asset directory and "shelves" is the wall page: games
# named Art or Shelves must not move in
taken: set[str] = set(out.values()) | {"art", "shelves"}
for key in sorted(games):
if key in out:
continue
@@ -211,6 +218,12 @@ main > .meta { color:var(--ink-soft-on-sky); }
.card { background:var(--board); border:1px solid var(--line);
border-radius:.8rem; padding:.9rem 1.1rem; margin-top:1rem; }
.desc { white-space:pre-wrap; }
.wall { display:flex; flex-direction:column; gap:.5rem; margin:.7rem 0 1.4rem; }
.orow { display:flex; gap:.5rem; }
.ocell { flex-basis:0; min-width:0; background:#fff;
border:1px solid var(--line); border-radius:.6rem; padding:.5rem .6rem; }
.ozone { font-size:.72rem; color:var(--accent); margin-left:.3rem; }
.ogames { margin:.3rem 0 0; font-size:.82rem; line-height:1.5; }
footer { margin:2rem 0 1rem; color:var(--ink-soft-on-sky); font-size:.8rem;
text-align:center; }
footer a { color:var(--accent); }
@@ -372,6 +385,83 @@ def _card(g: dict, slug: str, art: dict[str, str]) -> str:
</a>"""
def _rows_of(openings: list[dict]) -> list[list[dict]]:
# mirror of unitRows() in templates/pages/shelves.html — keep in sync
rows: list[list[dict]] = []
key = None
for o in openings:
m = re.match(r"^([A-Za-z]+)\d+$", o.get("label") or "")
row_key = m.group(1).upper() if m else f"single-{o['id']}"
if row_key != key:
rows.append([])
key = row_key
rows[-1].append(o)
return rows
def _shelves_body(
units: list[dict],
locations: dict,
games: dict[str, dict],
slugs: dict[str, str],
) -> str:
"""The wall, drawn for the public page: units as their physical rows,
each opening listing its residents as links."""
openings = opening_index(units)
residents: dict[str, list[tuple[str, dict]]] = {}
for key, entry in games.items():
oid, _, _via = effective_location(key, entry, games, locations)
if oid and oid in openings:
residents.setdefault(oid, []).append((key, entry))
parts = []
for unit in units:
cells_rows = []
for row in _rows_of(unit.get("openings", [])):
cells = []
for o in row:
here = sorted(
residents.get(o["id"], []),
key=lambda p: (p[1].get("name") or "").casefold(),
)
links = " · ".join(
f'<a href="../{slugs[k]}/">{html.escape(e.get("name") or "?")}</a>'
for k, e in here
)
zone = (
f'<span class="ozone">{html.escape(o.get("zone") or "")}</span>'
if o.get("zone")
else ""
)
desc = (
f'<p class="meta">{html.escape(o["description"])}</p>'
if o.get("description")
else ""
)
width = (o.get("width_in") or 13.25) * 4
cells.append(
f'<div class="ocell" style="flex-grow:{width:.0f}">'
f"<b>{html.escape(o['label'])}</b> {zone}{desc}"
f'<p class="ogames">{links or "<span class=meta>empty</span>"}</p>'
"</div>"
)
cells_rows.append(f'<div class="orow">{"".join(cells)}</div>')
desc = (
f'<p class="meta">{html.escape(unit["description"])}</p>'
if unit.get("description")
else ""
)
parts.append(
f"<h2>{html.escape(unit['name'])}</h2>{desc}"
f'<div class="wall">{"".join(cells_rows)}</div>'
)
return (
'<p><a href="../">← the whole library</a></p>'
"<h1>The shelves</h1>"
'<p class="meta">Where everything lives — drawn as the wall stands.</p>'
+ "".join(parts)
)
def _detail_body(g: dict, art: dict[str, str]) -> str:
art_html = (
f'<img src="../art/{html.escape(art["full"])}" '
@@ -464,6 +554,7 @@ def run_export(
out_dir: Path,
*,
title: str = "My Game Library",
include_shelves: bool = True,
fetch: httpx.Client | None = None,
sleep=time.sleep,
) -> dict:
@@ -517,17 +608,70 @@ def run_export(
'"Powered by BGG" link; save the official badge from '
"boardgamegeek.com/applications to upgrade it"
)
# the location layer, when the owner has one and wants it published
units = load_furniture(cfg) if include_shelves else []
locations = load_locations(cfg) if include_shelves else {}
openings = opening_index(units)
where: dict[str, str] = {}
contains: dict[str, list[str]] = {}
for key, game in games.items():
oid, note, via = effective_location(key, game, games, locations)
opening = openings.get(oid) if oid else None
if opening:
where[key] = f"{opening['unit']} · {opening['label']}"
elif via is None and note:
where[key] = note
if game.get("stored_in"):
for other_key, other in games.items():
if str(other.get("bgg_id")) == str(game["stored_in"]):
contains.setdefault(other_key, []).append(key)
# game pages FIRST, index last: a killed run must not leave an index
# linking to pages that were never written
for key, game in games.items():
page_dir = out_dir / slugs[key]
page_dir.mkdir(exist_ok=True)
cover = art.get(key, {}).get("full", "")
extras = ""
if where.get(key):
extras += (
'<div class="card"><div class="factrow"><b>where it lives</b> '
f"{html.escape(where[key])}</div></div>"
)
container_key = next(
(
k
for k, other in games.items()
if game.get("stored_in")
and str(other.get("bgg_id")) == str(game["stored_in"])
),
None,
)
if container_key:
extras += (
'<div class="card"><div class="factrow"><b>where it lives</b> '
f'inside <a href="../{slugs[container_key]}/">'
f"{html.escape(games[container_key].get('name') or '?')}</a>"
"</div></div>"
)
if contains.get(key):
links = " · ".join(
f'<a href="../{slugs[k]}/">'
f"{html.escape(games[k].get('name') or '?')}</a>"
for k in sorted(
contains[key],
key=lambda c: (games[c].get("name") or "").casefold(),
)
)
extras += (
'<div class="card"><div class="factrow"><b>in this box</b> '
f"{links}</div></div>"
)
atomic_write_text(
page_dir / "index.html",
_page(
f"{game.get('name') or 'game'} · {title}",
_detail_body(game, art.get(key, {})),
_detail_body(game, art.get(key, {})) + extras,
rel="../",
site_title=title,
badge_present=badge_present,
@@ -546,6 +690,25 @@ def run_export(
'<p class="meta" id="nomatch" hidden>No games match that search.</p>'
f"{_INDEX_JS}"
)
if units:
shelves_dir = out_dir / "shelves"
shelves_dir.mkdir(exist_ok=True)
atomic_write_text(
shelves_dir / "index.html",
_page(
f"The shelves · {title}",
_shelves_body(units, locations, games, slugs),
rel="../",
site_title=title,
badge_present=badge_present,
),
)
index_body = index_body.replace(
'<div class="controls">',
'<p><a href="shelves/">see the shelves — where everything lives →</a></p>'
'<div class="controls">',
1,
)
atomic_write_text(
out_dir / "index.html",
_page(title, index_body, rel="", site_title=title, badge_present=badge_present),
+157 -15
View File
@@ -178,29 +178,171 @@ def effective_location(
return loc.get("opening_id"), loc.get("note", ""), via if via != key else None
def opening_report(opening: dict, games_here: list[tuple[str, dict]]) -> dict:
"""Capacity picture for one opening: fill (stacked thickness vs
interior height), misfits, and how many boxes are unmeasured. Games
stored INSIDE another box are listed residents but occupy no shelf
space of their own — only their container is stacked and fit-checked,
the same stance the dims report takes."""
physical = [(k, e) for k, e in games_here if not e.get("stored_in")]
thicknesses = [stack_thickness(e) for _, e in physical]
stacked = sum(t for t in thicknesses if t)
unmeasured = sum(1 for t in thicknesses if t is None)
def _flat_claim(dims: tuple, opening: dict) -> float | None:
"""A flat box's claim on the opening's WIDTH: its footprint (the two
larger axes) must fit width × depth; the claim is the footprint axis
left running along the width, minimized over the two orientations.
None = the footprint fits neither way."""
t = min(dims)
rest = sorted(dims)
rest.remove(t)
a, b = rest # a <= b
claims = []
if b <= opening["depth_in"] and a <= opening["width_in"]:
claims.append(a)
if a <= opening["depth_in"] and b <= opening["width_in"]:
claims.append(b)
return min(claims) if claims else None
def _stands(dims: tuple, opening: dict) -> bool:
"""On edge, the thinnest axis runs along the width; the other two
must fit height × depth one way or the other."""
t = min(dims)
rest = sorted(dims)
rest.remove(t)
a, b = rest
return (a <= opening["height_in"] and b <= opening["depth_in"]) or (
b <= opening["height_in"] and a <= opening["depth_in"]
)
def lane_split(opening: dict, physical: list[tuple[str, dict, str]]) -> dict:
"""The two-lane packing model. Each opening holds a flat stack
(thinnest axes summing against interior HEIGHT, claiming the widest
flat box's width) and a standing lane beside it (boxes on edge,
thinnest axes summing against the REMAINING width). Lanes are chosen
greedily — larger footprints lie flat, smaller boxes stand — unless
an assignment carries an explicit lane override. A box no lane can
take is laneless; the opening warns only then.
physical: [(key, entry, override)] with override in "", "flat",
"standing". Unmeasured boxes take no lane but are counted."""
height = opening.get("height_in")
width = opening.get("width_in")
lanes: dict[str, str] = {}
laneless: list[str] = []
unmeasured = 0
flat_in = 0.0
flat_claim = 0.0
standing: list[tuple[str, float]] = [] # (key, thickness)
def footprint(dims):
axes = sorted(dims, reverse=True)
return axes[0] * axes[1]
measured = []
for key, entry, override in physical:
dims = game_dims(entry)
if dims is None:
unmeasured += 1
else:
measured.append((key, entry, override, dims))
measured.sort(key=lambda item: footprint(item[3]), reverse=True)
def try_flat(key, dims):
nonlocal flat_in, flat_claim
claim = _flat_claim(dims, opening)
thickness = min(dims)
if claim is None or flat_in + thickness > height:
return False
flat_in += thickness
flat_claim = max(flat_claim, claim)
lanes[key] = "flat"
return True
def try_standing(key, dims):
thickness = min(dims)
used = sum(t for _, t in standing)
if not _stands(dims, opening) or used + thickness > width - flat_claim:
return False
standing.append((key, thickness))
lanes[key] = "standing"
return True
max_fp = footprint(measured[0][3]) if measured else 0
for key, entry, override, dims in measured:
if override == "flat":
placed = try_flat(key, dims)
elif override == "standing":
placed = try_standing(key, dims)
elif footprint(dims) >= max_fp - 1e-6:
# the largest-footprint class lies flat; smaller boxes stand
# beside the stack when they can
placed = try_flat(key, dims) or try_standing(key, dims)
else:
placed = try_standing(key, dims) or try_flat(key, dims)
if not placed:
laneless.append(entry.get("name") or "?")
# the flat claim can grow after standing boxes were placed: re-check
while standing and sum(t for _, t in standing) > width - flat_claim:
key, _ = standing.pop()
laneless.append(
next((e.get("name") or "?" for k, e, _o in physical if k == key), "?")
)
lanes.pop(key, None)
return {
"lanes": lanes,
"flat_in": round(flat_in, 2),
"flat_claim": round(flat_claim, 2),
"standing_in": round(sum(t for _, t in standing), 2),
"laneless": laneless,
"unmeasured": unmeasured,
}
def opening_report(
opening: dict,
games_here: list[tuple[str, dict]],
overrides: dict[str, str] | None = None,
) -> dict:
"""Capacity picture for one opening under the two-lane model. Games
stored INSIDE another box are listed residents but occupy no shelf
space of their own — only their container is packed, the same stance
the dims report takes."""
overrides = overrides or {}
physical = [
(k, e, overrides.get(k, "")) for k, e in games_here if not e.get("stored_in")
]
height = opening.get("height_in")
width = opening.get("width_in")
if not (height and width):
return {
"games": len(physical),
"flat_in": 0,
"standing_in": 0,
"flat_claim": 0,
"lanes": {},
"unmeasured": sum(1 for _, e, _o in physical if game_dims(e) is None),
"fill": 0,
"standing_fill": 0,
"overfull": False,
"misfits": [],
"laneless": [],
}
split = lane_split(opening, physical)
misfits = [
entry.get("name") or "?"
for _, entry in physical
for _, entry, _o in physical
if fits_opening(entry, opening) is False
]
standing_budget = max(width - split["flat_claim"], 0.01)
return {
"games": len(physical),
"stacked_in": round(stacked, 2),
"unmeasured": unmeasured,
"fill": round(stacked / height, 2) if height and stacked else 0,
"overfull": bool(height and stacked > height),
"flat_in": split["flat_in"],
"standing_in": split["standing_in"],
"flat_claim": split["flat_claim"],
"lanes": split["lanes"],
"unmeasured": split["unmeasured"],
"fill": round(split["flat_in"] / height, 2) if split["flat_in"] else 0,
"standing_fill": (
round(split["standing_in"] / standing_budget, 2)
if split["standing_in"]
else 0
),
"overfull": bool(split["laneless"]),
"misfits": misfits,
"laneless": split["laneless"],
}
+6
View File
@@ -581,6 +581,12 @@ button.opening:hover, button.opening:focus-visible {
.fillbar > div { height: 100%; background: var(--go, #2f8a45);
border-radius: 1rem; }
.fillbar > div.over { background: #b3372f; }
.fillbar.dual { margin-top: 2px; }
.fillbar > div.stand { background: #3e6fa3; }
.lanebtn { font: inherit; font-size: .72rem; cursor: pointer;
border: 1px solid var(--board-edge); border-radius: 1rem;
background: #fff; padding: .05rem .5rem; color: var(--ink-soft); }
.lanebtn:hover { border-color: var(--accent); color: var(--accent); }
.unshelved-row { display: flex; gap: .6rem; align-items: center;
flex-wrap: wrap; padding: .4rem 0;
border-bottom: 1px solid var(--board-edge); }
+1 -1
View File
@@ -30,7 +30,7 @@
<p><b><a href="/titles">Titles</a></b> — every read off your shelves, alphabetized, with its status and photos. This is the proofread checkpoint: <a href="#curation">edit, split, remove</a>. Its badge counts <span class="chip shaky">shaky read</span> lines — the model wasn't sure and nothing has verified them; filter to them, then press <b>✓ looks right</b> or edit each one.</p>
<p><b><a href="/review">Review</a></b> — 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 (<b>keep locally</b>: it joins the Library, never uploads). Keyboard-first; see <a href="#keys">shortcuts</a>.</p>
<p><b><a href="/queue">Queue</a></b> — 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 <b>retry N failed</b> checkbox. Each queued row shows what upload did with it — <span class="chip open">pending</span>, <span class="chip ok">done</span>, <span class="chip no">failed</span>, or <span class="chip no">retired</span> (a review decision since the last diff withdrew it). Finished rows stay listed until the next <b>diff</b> rebuilds the queue; the log below them is the permanent record.</p>
<p><b><a href="/shelves">Shelves</a></b> — where everything physically lives, drawn as it really stands: openings group into rows by label letter with widths proportional to their interiors, so the page is a diagram of your wall. Describe furniture once (presets for Kallax cubes and Billy shelves, grids of rows × columns — a later grid continues the row letters — custom sizes, and no-size spots like a travel case); units and openings both take a free-text description beside the zone keyword. Suggestions rank an opening holding a <b>series-mate or base game first</b> (marked ♥ — every placement you confirm teaches the suggester your organization), always against the <i>remaining</i> room, then tightest verified fit; a measured game with nowhere suitable says "no matching openings yet", and unmeasured games get no suggestions because can't-verify ≠ fits. Assign by tapping suggestions, searching inside an opening, or from any game's detail page. Fill bars show stacked thickness against interior height; ⚠ flags overfull openings and boxes that can't fit; games stored inside another box ride along with their container, through whole chains. Your layout is local data only — never sent anywhere. Bulk-load a plan with <code>bggpipe shelve --import plan.csv</code> (name,opening) and check capacity anytime with <code>bggpipe dims</code>.</p>
<p><b><a href="/shelves">Shelves</a></b> — where everything physically lives, drawn as it really stands: openings group into rows by label letter with widths proportional to their interiors, so the page is a diagram of your wall. Describe furniture once (presets for Kallax cubes and Billy shelves, grids of rows × columns — a later grid continues the row letters — custom sizes, and no-size spots like a travel case); units and openings both take a free-text description beside the zone keyword. Suggestions rank an opening holding a <b>series-mate or base game first</b> (marked ♥ — every placement you confirm teaches the suggester your organization), always against the <i>remaining</i> room, then tightest verified fit; a measured game with nowhere suitable says "no matching openings yet", and unmeasured games get no suggestions because can't-verify ≠ fits. Assign by tapping suggestions, searching inside an opening, or from any game's detail page. Capacity is a two-lane model — a flat stack against interior height plus a standing lane against the remaining width; the largest boxes lie flat, smaller ones stand, and each box's lane is flippable in the opening's view. ⚠ means <i>no packing fits</i>, not merely "tall stack"; games stored inside another box ride along with their container, through whole chains. Your layout is local data only — never sent anywhere. Bulk-load a plan with <code>bggpipe shelve --import plan.csv</code> (name,opening) and check capacity anytime with <code>bggpipe dims</code>.</p>
<p><b><a href="/library">Library</a></b> — 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, <b>your</b> 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 <code>data/</code> and folded in by the next <b>enrich</b>).</p>
</div>
+25 -8
View File
@@ -80,18 +80,25 @@ function openingById(id) {
function fillBar(o) {
if (!o.width_in) return `<span class="meta">no size limits</span>`;
const pct = Math.min(100, Math.round((o.fill || 0) * 100));
return `<div class="fillbar" role="img"
aria-label="${pct}% full${o.overfull ? ", overfull" : ""}${
o.unmeasured ? `, ${o.unmeasured} unmeasured boxes not counted` : ""}">
<div style="width:${pct}%" class="${o.overfull ? "over" : ""}"></div>
const flat = Math.min(100, Math.round((o.fill || 0) * 100));
const stand = Math.min(100, Math.round((o.standing_fill || 0) * 100));
const label = `flat stack ${flat}% of height, standing lane ${stand}% of width`
+ (o.overfull ? ", overfull" : "")
+ (o.unmeasured ? `, ${o.unmeasured} unmeasured boxes not counted` : "");
return `<div class="fillbar dual" role="img" aria-label="${esc(label)}">
<div style="width:${flat}%" class="${o.overfull ? "over" : ""}"
title="flat stack: ${flat}% of interior height"></div>
</div>
<div class="fillbar dual" role="img" aria-hidden="true">
<div style="width:${stand}%" class="stand ${o.overfull ? "over" : ""}"
title="standing lane: ${stand}% of remaining width"></div>
</div>`;
}
function openingCell(o) {
const why = [
o.overfull ? "stacked past the interior height" : "",
o.misfits.length ? "doesn't fit: " + o.misfits.join(", ") : "",
o.overfull ? `no way to pack: ${o.laneless.join(", ")}` : "",
o.misfits.length ? "doesn't fit this opening at all: " + o.misfits.join(", ") : "",
].filter(Boolean).join("; ");
const warn = why
? `<span class="chip no" role="img" aria-label="${esc(why)}" title="${esc(why)}">⚠</span>`
@@ -218,6 +225,12 @@ function sheetRender() {
<a href="/library/game/${encodeURIComponent(g.key)}">${esc(g.name)}</a>
${g.measured ? "" : `<span class="chip open"
title="no trusted size — not counted in the fill bar">unmeasured</span>`}
${g.lane && !g.inherited
? `<button class="lanebtn" data-key="${esc(g.key)}"
data-next="${g.lane === "flat" ? "standing" : "flat"}"
title="how this box sits — tap to flip${g.lane_override ? " (your override)" : ""}">
${g.lane === "flat" ? "▬ flat" : "▮ standing"}${g.lane_override ? " ✱" : ""}</button>`
: ""}
${g.inherited
? `<span class="meta">inside another box here</span>`
: `<button class="unassign" data-key="${esc(g.key)}">remove</button>`}
@@ -350,7 +363,11 @@ document.getElementById("opensheet").addEventListener("click", e => {
});
document.getElementById("sheetgames").addEventListener("click", e => {
const b = e.target.closest(".unassign");
if (b) post("/api/locate", {key: b.dataset.key, opening_id: ""});
if (b) { post("/api/locate", {key: b.dataset.key, opening_id: ""}); return; }
const lane = e.target.closest(".lanebtn");
if (lane) post("/api/locate", {
key: lane.dataset.key, opening_id: OPEN, lane: lane.dataset.next,
});
});
document.getElementById("sheetsearch").addEventListener("input", () => {
const q = document.getElementById("sheetsearch").value.trim().toLowerCase();
+19 -7
View File
@@ -75,7 +75,6 @@ from bggpipe.shelves import (
related,
save_furniture,
save_locations,
stack_thickness,
)
@@ -268,6 +267,7 @@ class LocateBody(BaseModel):
key: str
opening_id: str = "" # "" = unshelve
note: str = ""
lane: str = "" # "", "flat", "standing": how THIS box sits in the opening
class StoredInBody(BaseModel):
@@ -1097,13 +1097,16 @@ def create_app(
lost_home.add(key)
elif via is not None:
located_keys.add(key) # rides with its container
lane_overrides = {
k: v.get("lane", "") for k, v in locations.items() if v.get("lane")
}
out_units = []
for unit in units:
unit = {**unit, "description": unit.get("description", "")}
out_openings = []
for opening in unit.get("openings", []):
here = residents.get(opening["id"], [])
report = opening_report(opening, here)
report = opening_report(opening, here, lane_overrides)
out_openings.append(
{
**opening,
@@ -1115,6 +1118,8 @@ def create_app(
"thumbnail": e.get("thumbnail") or "",
"inherited": bool(e.get("stored_in")),
"measured": game_dims(e) is not None,
"lane": report["lanes"].get(k, ""),
"lane_override": lane_overrides.get(k, ""),
}
for k, e in sorted(
here,
@@ -1145,7 +1150,6 @@ def create_app(
# budget already spent is subtracted before offering, so
# honesty holds as shelves fill. Then tightest fit, so a
# small box is offered cubes, never the oversize row.
thickness = stack_thickness(entry)
fitting = []
for oid, opening in openings.items():
if not opening.get("width_in"):
@@ -1153,9 +1157,14 @@ def create_app(
if fits_opening(entry, opening) is not True:
continue
here = residents.get(oid, [])
report = opening_report(opening, here)
remaining = opening["height_in"] - report["stacked_in"]
if thickness is None or thickness > remaining:
# "fits" means the REMAINING opening: an opening whose
# current packing already fails is never offered, and the
# game must find a lane in the split with itself included
current = opening_report(opening, here, lane_overrides)
if current["laneless"]:
continue
probe = opening_report(opening, [*here, (key, entry)], lane_overrides)
if (entry.get("name") or "?") in probe["laneless"]:
continue
reunites = any(related(entry, e) for _, e in here)
volume = (
@@ -1413,10 +1422,13 @@ def create_app(
if opening_id and opening_id not in opening_index(load_furniture(cfg)):
raise HTTPException(400, "no such opening")
if opening_id or body.note.strip():
locations[body.key] = {
record = {
"opening_id": opening_id or None,
"note": body.note.strip(),
}
if body.lane in ("flat", "standing"):
record["lane"] = body.lane
locations[body.key] = record
else:
locations.pop(body.key, None)
save_locations(cfg, locations)
+1 -1
View File
@@ -94,7 +94,7 @@ def test_report_buckets_misfits_and_unknowns(tmp_path, capsys):
summary = run_dims_report(cfg)
assert summary["misfits"] == ["Monster Box"]
out = capsys.readouterr().out
assert "doesn't fit: Monster Box" in out
assert "Monster Box" in out # named as unplaceable/misfit
assert "unshelved" in out # the conflicting + absent games have no home
assert "can't be size-verified" in out and "Never Measured" in out
+60 -2
View File
@@ -312,7 +312,7 @@ def test_cli_export_wiring(tmp_path, monkeypatch):
received = {}
def fake_run_export(cfg, out, *, title):
def fake_run_export(cfg, out, *, title, include_shelves=True):
received.update(out=out, title=title)
return {"games": 0, "downloaded": 0, "failures": 0, "removed": 0}
@@ -325,7 +325,7 @@ def test_cli_export_wiring(tmp_path, monkeypatch):
assert received["title"] == "T" and received["out"] == tmp_path / "s"
assert runner.invoke(app, ["export"]).exit_code != 0 # --out is required
def failing_run_export(cfg, out, *, title):
def failing_run_export(cfg, out, *, title, include_shelves=True):
return {"games": 1, "downloaded": 0, "failures": 2, "removed": 0}
monkeypatch.setattr("bggpipe.export.run_export", failing_run_export)
@@ -346,3 +346,61 @@ def test_colophon_card_closes_the_shelf(tmp_path):
assert (out / "art" / "bggpipe-piper.jpg").exists()
# the filter only ever hides <a class="game"> — the colophon is a div
assert '<div class="game colophon"' in index
def test_shelves_export_and_optout(tmp_path):
"""The wall ships with the site — locations on game pages, a shelves
page drawn as the physical arrangement unless the owner opts out
(publishing a shelf layout is a choice)."""
from bggpipe.shelves import save_furniture, save_locations
cfg = _cfg(tmp_path)
save_furniture(
cfg,
[
{
"name": "Den Kallax",
"description": "the wall behind the couch",
"openings": [
{
"id": "den-a1",
"label": "A1",
"zone": "oversize",
"width_in": 26.5,
"height_in": 13.25,
"depth_in": 15.4,
},
{
"id": "den-b1",
"label": "B1",
"zone": "cubes",
"width_in": 13.25,
"height_in": 13.25,
"depth_in": 15.4,
},
],
}
],
)
save_locations(cfg, {"240:24621": {"opening_id": "den-b1", "note": ""}})
out = tmp_path / "site"
run_export(cfg, out, fetch=_cdn([]), sleep=lambda s: None)
shelves = (out / "shelves" / "index.html").read_text()
assert "Den Kallax" in shelves and "the wall behind the couch" in shelves
assert "Britannia" in shelves # the resident links from its opening
assert 'href="../britannia/"' in shelves
detail = (out / "britannia" / "index.html").read_text()
assert "where it lives" in detail and "Den Kallax · B1" in detail
# containment renders on both pages
local = (out / "britannia-2" / "index.html").read_text()
assert "in this box" not in local # the localgame contains nothing
index = (out / "index.html").read_text()
assert "see the shelves" in index
# opt-out: no shelves page, no location lines, no link
out2 = tmp_path / "site2"
run_export(cfg, out2, include_shelves=False, fetch=_cdn([]), sleep=lambda s: None)
assert not (out2 / "shelves").exists()
assert "where it lives" not in (out2 / "britannia" / "index.html").read_text()
assert "see the shelves" not in (out2 / "index.html").read_text()
+45 -10
View File
@@ -59,23 +59,58 @@ def test_fits_opening_speaks_the_house_language():
assert fits_opening(big, TRAVEL) is None
def test_opening_report_counts_stack_and_warns():
def test_opening_report_packs_two_lanes():
thin = _entry("Thin", 11, 11, 2)
thick = _entry("Thick", 11, 11, 4)
unmeasured = _entry("Mystery", w=None)
report = opening_report(KALLAX, [("a", thin), ("b", thick), ("c", unmeasured)])
assert report["games"] == 3
assert report["stacked_in"] == 6
assert report["flat_in"] == 6 # both lie flat: 2 + 4 within 13.25
assert report["unmeasured"] == 1
assert report["overfull"] is False
# seven thick boxes overflow a 13.25" interior
stack = [(str(i), thick) for i in range(7)]
assert opening_report(KALLAX, stack)["overfull"] is True
# a misfit is named even when the stack has room
# a misfit is named even when the lanes have room
report = opening_report(KALLAX, [("a", _entry("Too Long", 11.5, 17, 2.25))])
assert report["misfits"] == ["Too Long"]
def test_reviewer_lane_cases():
"""The two acceptance cases: 3 flat + 4 standing in a 26.8" wide
passes; 12 standard boxes FORCED all-flat in a cube still warns."""
wide = {
"id": "w",
"label": "top",
"zone": "",
"width_in": 26.8,
"height_in": 13.25,
"depth_in": 15.4,
}
big = [_entry(f"Big{i}", 11.6, 11.6, 2.8) for i in range(3)]
small = [_entry(f"Small{i}", 11.5, 9, 2) for i in range(4)]
report = opening_report(
wide,
[(f"b{i}", g) for i, g in enumerate(big)]
+ [(f"s{i}", g) for i, g in enumerate(small)],
)
assert report["overfull"] is False and report["laneless"] == []
assert report["flat_in"] == 8.4 # 3 × 2.8 lying flat
assert report["standing_in"] == 8 # 4 × 2 on edge beside the stack
assert sorted(set(report["lanes"].values())) == ["flat", "standing"]
# 12 standard boxes forced flat: only ~4 fit the height; warn
twelve = [(f"g{i}", _entry(f"G{i}", 11.6, 11.6, 2.8)) for i in range(12)]
forced = opening_report(
KALLAX, twelve, overrides={f"g{i}": "flat" for i in range(12)}
)
assert forced["overfull"] is True
assert len(forced["laneless"]) == 8 # 4 fit the 13.25" height, 8 cannot
# unforced, the same twelve mostly still can't fit a single cube —
# flat takes 4, standing takes what remains of the width, rest warn
free = opening_report(KALLAX, twelve)
assert free["overfull"] is True
assert len(free["laneless"]) > 0
def test_effective_location_inherits_from_container():
games = {
"173634": _entry("Trove", bgg_id=173634),
@@ -401,7 +436,7 @@ def test_stored_in_chain_resolves_to_the_outermost_box(tmp_path):
assert {g["name"] for g in resident} == {"Trove", "Insert", "Minis"}
# only the physical box is stacked and counted
assert state["units"][0]["openings"][0]["games"] == 1
assert state["units"][0]["openings"][0]["stacked_in"] == 4
assert state["units"][0]["openings"][0]["flat_in"] == 4
assert state["unshelved"] == []
# deep cycles refuse: Trove into Minis would close the loop
@@ -787,9 +822,9 @@ def test_reunification_outranks_tightest_fit_and_full_shelves_stop_lying(tmp_pat
state = web.get("/api/shelves").json()
exp = next(g for g in state["unshelved"] if g["name"].endswith("Tower"))
# A1 holds the series-mate BUT its remaining height (13.25 - 3 - 11 < 2)
# can't take the expansion: honesty beats reunification, so the empty
# twin cube leads and A1 is not offered at all
# A1 holds the series-mate BUT its packing already fails (the base
# is laneless behind the hog): a broken opening is never offered, so
# honesty beats reunification and the empty twin cube leads
offered = [s["id"] for s in exp["suggestions"]]
assert ids["A1"] not in offered
assert offered[0] == ids["A2"]