Problem
Board game collectors typically track their games in spreadsheets or plain text lists. These flat formats make it impossible to browse visually, filter by player count or play time, or quickly find a game for a specific group size. The collection grows but the tooling stays stuck at rows and columns, and manually entering metadata for each game is tedious enough that the list falls out of date.
Solution
Step 1: Photo-based game entry
Use a phone camera to photograph your shelf. Pass the image to a vision model to extract game titles.
async function identifyGamesFromPhoto(imageBase64) {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-5-20250929",
max_tokens: 1024,
messages: [{
role: "user",
content: [{
type: "image",
source: { type: "base64", media_type: "image/jpeg", data: imageBase64 },
}, {
type: "text",
text: "List every board game visible on this shelf. Return JSON: [{\"title\": \"...\"}]",
}],
}],
});
return JSON.parse(response.content[0].text);
}
Step 2: Enrich with metadata from BoardGameGeek
async function enrichGame(title) {
const searchUrl = `https://boardgamegeek.com/xmlapi2/search?query=${encodeURIComponent(title)}&type=boardgame`;
const searchResult = await fetch(searchUrl).then((r) => r.text());
const gameId = parseGameId(searchResult);
const detailUrl = `https://boardgamegeek.com/xmlapi2/thing?id=${gameId}&stats=1`;
const detail = await fetch(detailUrl).then((r) => r.text());
return {
title,
minPlayers: parseField(detail, "minplayers"),
maxPlayers: parseField(detail, "maxplayers"),
playingTime: parseField(detail, "playingtime"),
rating: parseField(detail, "average"),
thumbnail: parseThumbnail(detail),
};
}
Step 3: Render a virtual shelf UI
<div class="shelf">
<div class="shelf-row" v-for="row in shelfRows" :key="row.id">
<div
class="game-spine"
v-for="game in row.games"
:key="game.title"
:style="{ width: spineWidth(game), backgroundColor: game.color }"
@click="showDetail(game)"
>
<span class="spine-title">{{ game.title }}</span>
</div>
</div>
</div>
Step 4: Add search and filter controls
const filteredGames = computed(() => {
return games.value.filter((game) => {
const matchesSearch = game.title.toLowerCase().includes(query.value.toLowerCase());
const matchesPlayers = !playerCount.value
|| (game.minPlayers <= playerCount.value && game.maxPlayers >= playerCount.value);
const matchesTime = !maxTime.value || game.playingTime <= maxTime.value;
return matchesSearch && matchesPlayers && matchesTime;
});
});
Why It Works
Vision models reliably read game titles from shelf photos, eliminating the manual data entry bottleneck that causes collections to go stale. BoardGameGeek's XML API provides rich metadata for free, so each game automatically gets player count, play time, and ratings. The shelf visualization matches the physical mental model collectors already have, making browse-and-pick faster than scanning a spreadsheet.
Context
- BoardGameGeek's XML API v2 is free and unauthenticated but has rate limits so batch requests with delays
- Vision models handle angled spines and partial occlusion surprisingly well for standard box sizes
- The virtual shelf can be extended with sorting by rating, play count, or acquisition date
- Similar patterns work for book collections, vinyl records, or any physical media with identifiable spines
- Data persists in localStorage or a JSON file so no backend is needed for personal use