Skip to main content

PlyBench is a benchmark suite for evaluating the performance of LLMs and LLM Agents in simple game environments.

Project description

PlyBench

PlyBench is a benchmark suite for evaluating the performance of LLMs and LLM agents in simple, fully-observable game environments. It pits players (LLMs, MCTS, optimal solvers, random, or human) against each other across a matrix of games and records every step for later analysis.

Live results for a selection of models and games are available at plybench.jakubrada.com.

Installation

pip install plybench

Or with uv:

uv add plybench

Requires Python 3.12+.

Each LLM provider SDK is an optional extra — install only the ones you need (or all):

pip install "plybench[openai]"         # OpenAI
pip install "plybench[gemini]"         # Google Gemini
pip install "plybench[metacentrum]"    # Metacentrum (OpenAI-compatible endpoint)
pip install "plybench[huggingface]"    # local HuggingFace models (torch + transformers)
pip install "plybench[all]"            # everything

Providers whose SDK is not installed are simply skipped when building PlyBench().

Quickstart

Building an PlyBench object is the one-stop bootstrap: it creates an instance-scoped registry, registers the built-in games and players, and wires up the LLM router.

import asyncio

from plybench import PlyBench
from plybench.harness.benchmark import Benchmark

op = PlyBench()  # reads provider keys from the environment (see Configuration)

benchmark = Benchmark(
    experiment="quickstart",
    op=op,
    game_configs=["tic_tac_toe:"],
    player_configs=["random:distribution=uniform"],
    opponent_configs=["optimal:stochastic=True"],
    num_games=10,
)

results = asyncio.run(benchmark.run())

Runs are resumable and written under results/benchmarks/<experiment>/ in the current working directory; re-running skips already-completed rounds.

Config strings

Games and players are addressed by name:key=value:key=value strings, e.g. llm:actions:text:openai:gpt-5:thinking_enabled=True or random:distribution=uniform.

Built-in games: tic_tac_toe, modified_tic_tac_toe, magic_square, story_magic_square, nim, modified_nim, inverse_nim, story_nim, connect_four, breakthrough.

Built-in players: human, random, mcts, optimal, llm.

Extend either set at runtime via op.registry.register_game(...) / op.registry.register_player(...).

Configuration

LLM providers are configured through environment variables (a .env file is loaded automatically). PlyBench() self-disables any provider whose key is absent, so bot-only benchmarks run offline. See .env.example:

OPENAI_API_KEY=...
OPENAI_ORGANIZATION=...
OPENAI_PROJECT=...

GEMINI_API_KEY=...
GEMINI_PROJECT=...

METACENTRUM_BASE_URL=...
METACENTRUM_API_KEY=...

HF_TOKEN=...   # only for gated/private HuggingFace models

HuggingFace (local models)

The HuggingFace provider runs models locally instead of calling a remote API — it exposes embed() (generation is not supported yet). hf_models selects which of the supported models (see providers/huggingface/models.py) this environment uses; they are downloaded/verified into the local HF cache at bootstrap:

op = PlyBench(hf_models=["sup-simcse-bert"])
resp = await op.llm.embed(Provider.HUGGINGFACE, "sup-simcse-bert", ["hello"])

Requesting a supported model that was not part of hf_models raises an error telling you to add it to the bootstrap list. Needs the huggingface extra installed.

Extending PlyBench

Games and players are open registries on op.registry — you can add your own from your own code without modifying the package. Each is registered as a spec that pairs a config-string key with the classes that implement it.

Adding a player

Implement two things:

  1. A PlayerParams subclass (configs/player_params.py) — the parsed form of your config string. Implement from_string / to_string / path_suffix. Reuse NoGameParams-style emptiness if your player is parameterless.
  2. A Player subclass (player/player.py) — implement initialize_policy (one-time setup per game), the async __call__ (given the game, an InterfaceObservation, and the legal InterfaceActions, return a PlayerOutput — set action=None to forfeit), and format_llm_output.

Optionally, attach a PlayerTracker to persist extra per-step data onto each recorded GameStep.

Then register a PlayerSpec:

from dataclasses import dataclass

from plybench import PlyBench
from plybench.configs.player_config import PlayerConfig
from plybench.configs.player_params import PlayerParams
from plybench.core.game import TurnBasedGame
from plybench.core.interface import InterfaceAction, InterfaceObservation
from plybench.core.prompt_adapter import PromptAdapter
from plybench.player.player import Player, PlayerIdentifier, PlayerOutput
from plybench.player.spec import PlayerSpec


@dataclass(frozen=True, eq=True)
class FirstMoveParams(PlayerParams):
    @classmethod
    def from_string(cls, params_string: str) -> "FirstMoveParams":
        return cls()

    def to_string(self) -> str:
        return ""

    @property
    def path_suffix(self) -> str:
        return ""


class FirstMovePlayer(Player):
    def initialize_policy(self, game: TurnBasedGame, prompt_adapter_template: PromptAdapter) -> None:
        pass

    async def __call__(self, game: TurnBasedGame, observation: InterfaceObservation, legal_moves: list[InterfaceAction]) -> PlayerOutput:
        return PlayerOutput(action=legal_moves[0] if legal_moves else None)

    def format_llm_output(self, player_output: PlayerOutput) -> str:
        return ""


op = PlyBench()
op.registry.register_player(PlayerSpec("first", FirstMoveParams, lambda game, cfg, pid: FirstMovePlayer(cfg, pid)))
# usable anywhere as the config string "first:"

Adding a game

Games are backed by OpenSpiel: the underlying game must be loadable by pyspiel.load_game(...) (a built-in OpenSpiel game or a custom game you register with OpenSpiel). A new variant implements the same set of classes the built-ins do — use any game under src/plybench/games/ (e.g. tic_tac_toe/tic_tac_toe.py) as a template:

  • TurnBasedGame — binds a registry game_type key to an OpenSpiel game_name.
  • InterfaceTransformer — renders state/actions for both display and the LLM prompt.
  • InterfaceAction / InterfaceObservation — convert to and from OpenSpiel (from_openspiel / to_openspiel).
  • PromptAdapter — the game's head prompt and expected action format.
  • TurnBasedEngine — wires all of the above together (engine_factory: GameConfig -> TurnBasedEngine).
  • Optionally a GameParams subclass for parameterized variants (or reuse NoGameParams).

Then register a GameSpec:

from plybench.configs.game_params import NoGameParams
from plybench.games.spec import GameSpec

op.registry.register_game(
    GameSpec(
        key="my_game",
        params_cls=NoGameParams,  # or a custom GameParams subclass
        engine_factory=MyGameEngine,  # GameConfig -> TurnBasedEngine
        solvable=False,  # True enables minimax optimality/regret analysis (small trees only)
    )
)
# usable anywhere as the config string "my_game:"

Registered games and players work everywhere the built-ins do — in Benchmark, the analysis pipeline, and the scripts.

Reproducing the paper results

The experiment scripts under scripts/ are not part of the installed package — they are the tooling used to produce and reproduce the paper's results from a repository checkout. Use them when you want to re-run the exact benchmarks the paper reports, extend them with new models, or run the analysis and export pipelines on the resulting transcripts. Everything is resumable, so an interrupted run continues where it left off.

The complete experiment definitions from the paper are prepared under experiments/benchmarks/:

Experiment File Games
Tic-tac-toe family ttt.json tic_tac_toe, modified_tic_tac_toe, magic_square, story_magic_square
Nim family nim.json nim, modified_nim, inverse_nim, story_nim
Connect Four connect_four.json connect_four

Each file declares the full sweep — the LLM players, the opponents (random, mcts, optimal), and the number of rounds — with per-item enabled toggles so you can narrow a run without editing the sweep.

git clone https://github.com/radajakub/plybench.git
cd plybench
uv sync

# run a prepared experiment (reads experiments/benchmarks/ttt.json)
uv run python scripts/run.py --experiment ttt

# or an ad-hoc smoke run
uv run python scripts/run.py --name smoke \
    --games tic_tac_toe: \
    --players random:distribution=uniform \
    --opponents optimal:stochastic=True \
    --num-games 10

The LLM matchups require the relevant provider API keys (see Configuration) and will incur API cost; bot-vs-bot matchups run offline. Results are written under results/benchmarks/<experiment>/.

Then analyze or export the transcripts:

uv run python scripts/analyze.py --experiment ttt   # compute per-matchup statistics + confidence intervals
uv run python scripts/export.py --experiment ttt --out ttt.tar.gz   # export results for the PlyBench website
uv run python scripts/play.py --game tic_tac_toe: --i human: --o optimal:stochastic=True  # play interactively

The full result set is large (tens of thousands of game transcripts) and is not stored in this repository.

License

MIT © Jakub Rada

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plybench-1.0.0.tar.gz (170.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plybench-1.0.0-py3-none-any.whl (106.4 kB view details)

Uploaded Python 3

File details

Details for the file plybench-1.0.0.tar.gz.

File metadata

  • Download URL: plybench-1.0.0.tar.gz
  • Upload date:
  • Size: 170.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for plybench-1.0.0.tar.gz
Algorithm Hash digest
SHA256 99a5e670565dc1f5556d924c4e411142bbaa0561e64c70f59851e8839cbfe401
MD5 160e551d08453cba3148868096a2289d
BLAKE2b-256 ccd32e1d69bdd76be916e2785e9f0850824b4674e657c270c7f3b9143bbeb928

See more details on using hashes here.

Provenance

The following attestation bundles were made for plybench-1.0.0.tar.gz:

Publisher: publish.yml on radajakub/plybench

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file plybench-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: plybench-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 106.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for plybench-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90c6bda7a95388420b0827b84ac3f9af7f0e48eb2af05affce5513f341c05f27
MD5 25a10f31a5e58c8e435bceb6b7d4f9bd
BLAKE2b-256 c28422c142cffd9d758fd4efba258967d9f3e3376ad58213d453c79dc91d17e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for plybench-1.0.0-py3-none-any.whl:

Publisher: publish.yml on radajakub/plybench

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page