Skip to content

Games ~ Creating Fixtures Using BoardGameGeek API

Will Cravitz edited this page Nov 25, 2023 · 1 revision

Below is code to call the BGG API for information on games and to create corresponding fixtures. The output of running this script will be a file called games-fixtures-<fixture_size>.json. This file should be placed in /src/chigame/games/fixtures.

This code was initially written in issue #98 and updated in issue #283 to parse special XML characters.

import requests
import xml.etree.ElementTree as ET
import xml.sax.saxutils
import json

def bgg_get_game_details(bgg_ids):
    BGG_BASE_URL = "https://www.boardgamegeek.com/xmlapi2/"
    ids_string = ",".join(map(str, bgg_ids))
    details_url = f"{BGG_BASE_URL}thing?id={ids_string}&stats=1"
    details_response = requests.get(details_url)
    details_text = xml.sax.saxutils.unescape(details_response.text)
    try:
        details_root = ET.fromstring(details_text)
    # Skip the batch if it cannot be parsed after escaping special characters
    except: 
        return []

    games_data = []
    for item in details_root.findall("item"):
        # Extract game details as before, but for each item
        name_element = item.find(".//name[@type='primary']")
        if name_element is None:
            continue  # Skip games without a name

        # Retrieve the complexity value from the API response, convert it to a float,
        # and round it to two decimal places. If the value is not found, default to None.
        if item.find(".//averageweight") is not None:
            complexity_value = item.find(".//averageweight").get("value")
            rounded_complexity = (
                round(float(complexity_value), 2) if complexity_value else None
            )
        else:
            rounded_complexity = None

        # Construct a dictionary with the game's details, parsing various elements from the API response
        game_data = {
            "BGG_id": item.get("id"),
            "name": item.find(".//name").get("value"),
            "image": item.find(".//image").text
            if item.find(".//image") is not None
            else "/static/images/no_picture_available.png",
            "description": item.find(".//description").text,
            "year_published": int(item.find(".//yearpublished").get("value"))
            if item.find(".//yearpublished") is not None
            else None,
            "min_players": item.find(".//minplayers").get("value"),
            "max_players": item.find(".//maxplayers").get("value"),
            "expected_playtime": item.find(".//playingtime").get("value"),
            "min_playtime": item.find(".//minplaytime").get("value"),
            "max_playtime": item.find(".//maxplaytime").get("value"),
            "suggested_age": item.find(".//minage").get("value"),
            "complexity": rounded_complexity,  # The rounded complexity rating of the game
            # Missing fields: category, mechanics
        }

        games_data.append(game_data)

    return games_data


def create_fixture_file(num_entries, filename="fixture_file.json", batch_size=10):
    fixtures = []
    fetched_games = 0
    bgg_id = 1

    while fetched_games < num_entries:
        batch_ids = list(range(bgg_id, bgg_id + batch_size))
        games_data = bgg_get_game_details(batch_ids)

        for game_data in games_data:
            if fetched_games >= num_entries:
                break

            fixture_entry = {
                "model": "games.game",
                "pk": fetched_games + 1,
                "fields": {
                    "name": game_data.get("name", ""),
                    "description": game_data.get("description", ""),
                    "year_published": game_data.get("year_published", None),
                    "image": game_data.get("image", ""),
                    "rules": "",
                    "min_players": game_data.get("min_players", ""),
                    "max_players": game_data.get("max_players", ""),
                    "suggested_age": game_data.get("suggested_age", ""),
                    "expected_playtime": game_data.get("expected_playtime", ""),
                    "min_playtime": game_data.get("min_playtime", ""),
                    "max_playtime": game_data.get("max_playtime", ""),
                    "complexity": game_data.get("complexity", None),
                    "BGG_id": game_data.get("BGG_id", ""),
                    "categories": [], 
                    "mechanics": [], 
                },
            }

            fixtures.append(fixture_entry)
            fetched_games += 1
            print(f"Added game {fetched_games} of {num_entries}")

        bgg_id += batch_size

    with open(filename, "w", encoding="utf-8") as f:
        json.dump(fixtures, f, ensure_ascii=False, indent=4)


if __name__ == "__main__":
    # This will create a fixture with 50 games
    fixture_size = 50
    create_fixture_file(fixture_size, f"games-fixture-{fixture_size}.json")
Clone this wiki locally