Skip to main content

Event Structure

Events are the atomic units of gameplay. Every state transition produces one or more events that describe what happened. The Event dataclass from core.py:23-29 defines the structure:
@dataclass(frozen=True)
class Event:
    type: str
    entity: str
    payload: dict
    tick: int
    salience: Salience

Fields Explained

The event name (e.g., "MOVE", "EAT", "DIE_WALL").Defines what kind of transition occurred. Event types are domain-specific — Snake has different events than Pac-Man or Chess.
Which entity caused or experienced the event (e.g., "player", "food").Entities are defined by their behavior under collision. A “food” is whatever produces EAT events when touched.
Event-specific data (e.g., {"pos": (5, 7)}, {"length": 4}).Contains the details needed to encode the event as tokens. Can include positions, scores, directions, or any relevant state change.
Game tick when the event occurred.Events are bundled by tick — all events from a single game step share the same tick value. This creates temporal structure in the event stream.
Importance level for filtering (0-4).Not all events are equally important. Salience allows the encoder to filter out low-priority events if needed.

Salience Levels

Salience is an integer enum from core.py:15-20 that defines event importance:
class Salience(IntEnum):
    TICK = 0
    MOVEMENT = 1
    COLLISION = 2
    RULE_EFFECT = 3
    PHASE = 4

Hierarchy

1

TICK (0)

Time progression markers. Lowest priority.
2

MOVEMENT (1)

Basic movement events like INPUT_R and MOVE. Frequent but low-information.
3

COLLISION (2)

Interaction events like DIE_WALL or DIE_SELF. Higher priority because they change game state significantly.
4

RULE_EFFECT (3)

Consequence events like EAT, GROW, FOOD_SPAWN, SCORE. Core game mechanics.
5

PHASE (4)

Major game phase changes (start, end, level transitions). Highest priority.
The encoder can filter events by salience threshold. For example, salience_threshold=Salience.MOVEMENT keeps only movement and higher events, dropping pure TICK markers.

Game Interface Protocol

Any game can plug into the pipeline by implementing three methods:

reset() → State

Initialize a new game episode and return the starting state.
def reset(self) -> SnakeState:
    cx, cy = self.width // 2, self.height // 2
    direction = self.rng.choice(list(Action))
    head = (cx, cy)
    body = [head]
    food = self._spawn_food(body)
    self._state = SnakeState(
        head=head, body=body, direction=direction,
        food=food, score=0, alive=True, tick=0,
    )
    return self._state
From snake.py:14-24.

step(action) → (State, Events, Done)

Execute one action and return:
  • New state
  • List of events that occurred
  • Whether the episode is done
def step(self, action: Action) -> tuple[SnakeState, list[Event], bool]:
    s = self._state
    events = []
    tick = s.tick + 1
    
    # Input event
    events.append(Event(
        type=f"INPUT_{action.value[0]}",
        entity="player",
        payload={"action": action.value},
        tick=tick,
        salience=Salience.MOVEMENT,
    ))
    
    # ... compute new head, check collisions ...
    
    # Movement event
    events.append(Event(
        type="MOVE", entity="player",
        payload={"pos": new_head},
        tick=tick, salience=Salience.MOVEMENT,
    ))
    
    # If food eaten, emit EAT, GROW, FOOD_SPAWN, SCORE
    if ate:
        events.append(Event(
            type="EAT", entity="player",
            payload={"pos": new_head},
            tick=tick, salience=Salience.RULE_EFFECT,
        ))
        # ... more events ...
    
    return new_state, events, done
From snake.py:26-128. A single step can produce many events. Return valid actions for the current state.
def legal_actions(self, state: SnakeState) -> list[Action]:
    return [a for a in Action if a != OPPOSITE.get(state.direction)]
From snake.py:130-131. Snake can’t reverse direction instantly.
The game emits events, but doesn’t know anything about tokens. The codec layer handles translation.

Event Examples from Snake

Here are real event types from snake.py:

Input Event

Event(
    type="INPUT_R",
    entity="player",
    payload={"action": "RIGHT"},
    tick=5,
    salience=Salience.MOVEMENT
)
From snake.py:39-45. Records player input.

Movement Event

Event(
    type="MOVE",
    entity="player",
    payload={"pos": (6, 7)},
    tick=5,
    salience=Salience.MOVEMENT
)
From snake.py:95-99. Player moved to new position.

Collision Event (Death)

Event(
    type="DIE_WALL",
    entity="player",
    payload={"pos": (10, 5)},
    tick=23,
    salience=Salience.COLLISION
)
From snake.py:53-57. Hit the wall, game over.

Rule Effect Events (Eating)

When the player eats food, multiple events fire:
# Collision with food
Event(
    type="EAT",
    entity="player",
    payload={"pos": (8, 6)},
    tick=12,
    salience=Salience.RULE_EFFECT
)

# Consequence: snake grows
Event(
    type="GROW",
    entity="player",
    payload={"length": 4},
    tick=12,
    salience=Salience.RULE_EFFECT
)

# Consequence: new food spawns
Event(
    type="FOOD_SPAWN",
    entity="food",
    payload={"pos": (3, 2)},
    tick=12,
    salience=Salience.RULE_EFFECT
)

# Consequence: score increments
Event(
    type="SCORE",
    entity="player",
    payload={"score": 3},
    tick=12,
    salience=Salience.RULE_EFFECT
)
From snake.py:69-121. This is collision-defined semantics in action: food is defined by the event chain it triggers.

Tick Bundling

All events from a single step() call share the same tick value. The encoder groups them together:
TICK INPUT_R MOVE X6 Y7
TICK INPUT_R MOVE X7 Y7
TICK INPUT_D MOVE X7 Y8 EAT GROW LEN3 FOOD_SPAWN X2 Y3 SCORE V2
Each TICK token marks the start of a new time step. Events within a tick are causally related.

Event Stream to Token Sequence

The event stream is the boundary between the game and the model:
1

Game produces events

step() returns a list of Event objects
2

Events are grouped by tick

All events with tick=5 are bundled together
3

Encoder filters by salience

Low-priority events can be dropped
4

Events become tokens

Each event maps to 1-3 tokens (e.g., MOVE X7 Y8)
The codec (next page) handles the translation from events to tokens.

Game-Agnostic Design

The event stream protocol is intentionally minimal. Any game can implement reset(), step(), and legal_actions() without knowing anything about tokens or transformers.
From the README: “For now, the game never touches the model. The tokenization layer is where game-agnosticism lives.”
Snake, Pac-Man, Survivor, and Chess will all emit different event types, but the structure is the same:
  • Events capture state transitions
  • Salience marks importance
  • Tick numbers create temporal structure
  • Payload contains domain-specific details

Next Steps

Theory

Understand the Wittgensteinian foundation

Tokenization

See how events become token sequences

Build docs developers (and LLMs) love