Skip to content

Your first game

This tutorial runs a game between five RandomAgents, reads what happened, and then runs many games to get win rates.

Run one game

GameRunner deals roles, asks each agent for an action when it's their turn, and applies the actions until someone wins. Agents are asynchronous, so the runner is too:

import asyncio

from tyrant import GameRunner, RandomAgent


async def main():
    agents = [RandomAgent(uid) for uid in range(5)]
    runner = GameRunner(agents, seed=7)
    final_state = await runner.run()
    print(final_state.winner)


asyncio.run(main())

Each agent's uid becomes its player ID. A game needs 5 to 10 players. Passing seed makes the deal, the seating and the deck repeatable. RandomAgent itself uses Python's global random module, so for fully repeatable games also call random.seed(...).

run() returns the final GameState. Every model in Tyrant is a frozen dataclass, so you can keep this state around and read it safely.

See how it ended

Every public event is recorded in order in GameState.events. The last one is always a GameOverEvent with the winner and the WinCondition:

from tyrant.models import GameOverEvent

game_over = final_state.events[-1]
assert isinstance(game_over, GameOverEvent)
print(f"{game_over.winner} wins by {game_over.condition}")
fascist wins by fascist_policies

Print the first few events to follow the game:

for event in final_state.events[:3]:
    print(event)
NominationEvent(president_uid=2, chancellor_uid=4)
ElectionEvent(president_uid=2, chancellor_uid=4, votes=frozendict.frozendict({0: <Vote.JA: 'ja'>, ...}), result=<Vote.NEIN: 'nein'>)
NominationEvent(president_uid=3, chancellor_uid=0)

The event log guide lists every event type.

See what a player saw

The final state holds every hidden role. render_text shows the game from one player's seat, with everything else hidden:

from tyrant import render_text

print(render_text(final_state, 0))
# Secret Hitler: Player 0's view

## You
- Player 0
- Role: liberal
- Party: liberal
- Status: dead
- Known teammates: none

## Players
In seat order:
- Player 2: party unknown, role unknown, dead
...

This is the same text an LLM agent receives. See Rendering state.

Run many games

Random play is a useful baseline. examples/basic_usage.py plays 1,000 ten-player games and counts the wins:

import asyncio

from tyrant import GameRunner, Party, RandomAgent


async def main():
    liberal_win = 0
    fascist_win = 0

    for i in range(1000):
        players = tuple(RandomAgent(i) for i in range(10))
        runner = GameRunner(agents=players)
        end_state = await runner.run()
        if end_state.winner is Party.LIBERAL:
            liberal_win += 1
        else:
            fascist_win += 1

    print(f"Liberal wins: {liberal_win}\nFascist wins: {fascist_win}")


if __name__ == "__main__":
    asyncio.run(main())
uv run python examples/basic_usage.py

Next, write your own agent.