Running an LLM game¶
This tutorial runs a game where language models play each other through LLMAgent.
Install and set a key¶
Install the llm extra, which adds litellm:
pip install "tyrant[llm]"
litellm reads provider keys from its usual environment variables, such as ANTHROPIC_API_KEY, OPENAI_API_KEY or GEMINI_API_KEY. Set the one for your provider:
export ANTHROPIC_API_KEY=...
One model against random players¶
A five-player game with discussion takes a few hundred actions, so five LLMAgents make a few hundred model calls. To try things out cheaply, seat one LLMAgent with four RandomAgents:
import asyncio
from tyrant import GameRunner, RandomAgent
from tyrant.llm.agent import LLMAgent
async def main():
llm = LLMAgent(0, "anthropic/claude-haiku-5-5")
agents = [llm] + [RandomAgent(uid) for uid in range(1, 5)]
runner = GameRunner(agents, discussion_enabled=True)
final_state = await runner.run()
print(f"Winner: {final_state.winner}")
print(f"Tokens: {llm.prompt_tokens} in, {llm.completion_tokens} out")
print(f"Cost: ${llm.cost:.4f}")
print(f"Notes: {llm.notes}")
asyncio.run(main())
The model is any litellm model string. Anything else for litellm.acompletion, such as temperature or api_base, goes in completion_kwargs:
from frozendict import frozendict
agent = LLMAgent(0, "openai/gpt-5", completion_kwargs=frozendict(temperature=0.7))
What the model sees¶
Each turn the agent sends two messages:
SYSTEM_PROMPT, with the rules and the reply format. It's the same for every game, so providers can cache it.build_prompt, with the player's rendered view, their private notes, and their numbered options.
The model may reason first, then ends its reply with a JSON object such as {"action": 3, "message": "...", "notes": "..."}. parse_action turns that into one of the offered actions. If the reply can't be used, the agent raises, and on the retry it shows the model its rejected reply and the reason. The prompting guide covers this in detail.
A full LLM game¶
examples/llm_game.py seats five LLMAgents with discussion on and prints the winner, the faults, the token totals, the cost and each agent's final notes:
import asyncio
import os
import sys
from tyrant import GameRunner
from tyrant.llm.agent import LLMAgent
async def main():
model = os.environ.get("TYRANT_MODEL")
if not model:
sys.exit("Set TYRANT_MODEL to a litellm model, e.g. anthropic/claude-haiku-5-5")
agents = tuple(LLMAgent(uid, model) for uid in range(5))
runner = GameRunner(agents, discussion_enabled=True)
end_state = await runner.run()
prompt_tokens = sum(agent.prompt_tokens for agent in agents)
completion_tokens = sum(agent.completion_tokens for agent in agents)
cached_tokens = sum(agent.cached_tokens for agent in agents)
fallbacks = sum(fault.fell_back for fault in runner.faults)
print(f"Winner: {end_state.winner}")
print(f"Faults: {len(runner.faults)} ({fallbacks} fell back to a random action)")
for fault in runner.faults:
print(f" Player {fault.uid} in {fault.phase}: {fault.reason}")
print(f"Prompt tokens: {prompt_tokens} ({cached_tokens} cached)")
print(f"Completion tokens: {completion_tokens}")
print(f"Cost: ${sum(agent.cost for agent in agents):.4f}")
for agent in agents:
print(f"Player {agent.uid} notes: {agent.notes or '(none)'}")
if __name__ == "__main__":
asyncio.run(main())
export ANTHROPIC_API_KEY=...
TYRANT_MODEL=anthropic/claude-haiku-5-5 uv run --extra llm python examples/llm_game.py
Save the game¶
GameRunner records every action it applies. Write the replay to a file to review the game later:
from tyrant import write_replay
write_replay(runner.replay, "game.json.gz")
See Replays for the file format.