OmniPlay is a benchmark suite for evaluating the performance of LLMs and LLM Agents in simple game environments.
Project description
OmniPlay
OmniPlay 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 omniplay.jakubrada.com.
Installation
pip install omniplay
Or with uv:
uv add omniplay
Requires Python 3.12+.
Quickstart
Building an OmniPlay 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 omniplay import OmniPlay
from omniplay.harness.benchmark import Benchmark
op = OmniPlay() # 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).
OmniPlay() 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=...
Extending OmniPlay
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:
- A
PlayerParamssubclass (configs/player_params.py) — the parsed form of your config string. Implementfrom_string/to_string/path_suffix. ReuseNoGameParams-style emptiness if your player is parameterless. - A
Playersubclass (player/player.py) — implementinitialize_policy(one-time setup per game), the async__call__(given the game, anInterfaceObservation, and the legalInterfaceActions, return aPlayerOutput— setaction=Noneto forfeit), andformat_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 omniplay import OmniPlay
from omniplay.configs.player_config import PlayerConfig
from omniplay.configs.player_params import PlayerParams
from omniplay.core.game import TurnBasedGame
from omniplay.core.interface import InterfaceAction, InterfaceObservation
from omniplay.core.prompt_adapter import PromptAdapter
from omniplay.player.player import Player, PlayerIdentifier, PlayerOutput
from omniplay.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 = OmniPlay()
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/omniplay/games/ (e.g. tic_tac_toe/tic_tac_toe.py) as a template:
TurnBasedGame— binds a registrygame_typekey to an OpenSpielgame_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
GameParamssubclass for parameterized variants (or reuseNoGameParams).
Then register a GameSpec:
from omniplay.configs.game_params import NoGameParams
from omniplay.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/omniplay.git
cd omniplay
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 OmniPlay 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file omniplay-1.0.0.tar.gz.
File metadata
- Download URL: omniplay-1.0.0.tar.gz
- Upload date:
- Size: 126.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4047117d9a6aad7fa1e4811dae750efdd634d693d0079fa43d6314fde4cb01b2
|
|
| MD5 |
2218fac08d1e19cd67d9b2ef76950dba
|
|
| BLAKE2b-256 |
579581ae8565e981439a8dcfb69400ccca95701942ee2592ea78a9633802453a
|
Provenance
The following attestation bundles were made for omniplay-1.0.0.tar.gz:
Publisher:
publish.yml on radajakub/omniplay
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
omniplay-1.0.0.tar.gz -
Subject digest:
4047117d9a6aad7fa1e4811dae750efdd634d693d0079fa43d6314fde4cb01b2 - Sigstore transparency entry: 2278117990
- Sigstore integration time:
-
Permalink:
radajakub/omniplay@7e5c503d81ec538b4dc62a35b8ae7974bba268e8 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/radajakub
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7e5c503d81ec538b4dc62a35b8ae7974bba268e8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file omniplay-1.0.0-py3-none-any.whl.
File metadata
- Download URL: omniplay-1.0.0-py3-none-any.whl
- Upload date:
- Size: 88.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e52883a1fa41bfb32a3359d3ec7471c4b299622f7e28899a7ce3e610e74db025
|
|
| MD5 |
c177344ed581a705e56a37bdda16ed29
|
|
| BLAKE2b-256 |
fbd42b2ada4509ccee5ea67fdf94f1ce1df9cb18ff5d704d46c5efbe821167d0
|
Provenance
The following attestation bundles were made for omniplay-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on radajakub/omniplay
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
omniplay-1.0.0-py3-none-any.whl -
Subject digest:
e52883a1fa41bfb32a3359d3ec7471c4b299622f7e28899a7ce3e610e74db025 - Sigstore transparency entry: 2278118049
- Sigstore integration time:
-
Permalink:
radajakub/omniplay@7e5c503d81ec538b4dc62a35b8ae7974bba268e8 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/radajakub
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7e5c503d81ec538b4dc62a35b8ae7974bba268e8 -
Trigger Event:
release
-
Statement type: