Skip to main content

Overview

Game Grammar includes three agent implementations for automated gameplay. Each agent implements the same interface but uses different strategies.

Agent Interface

All agents implement:
def act(self, state: SnakeState, legal: list[Action]) -> Action
state
SnakeState
required
Current game state
List of legal actions (from game.legal_actions(state))
return
Action
The chosen action to execute

RandomAgent

class RandomAgent:
    def __init__(self, seed=None)
Selects a random legal action uniformly. Useful for baseline performance and generating diverse gameplay data.
seed
int | None
default:"None"
Random seed for reproducible behavior

Implementation

def act(self, state: SnakeState, legal: list[Action]) -> Action:
    return self.rng.choice(legal)

Usage Example

from game_grammar import SnakeGame, RandomAgent

game = SnakeGame(width=10, height=10, seed=42)
agent = RandomAgent(seed=42)

state = game.reset()
for _ in range(100):
    legal = game.legal_actions(state)
    action = agent.act(state, legal)
    state, events, done = game.step(action)
    if done:
        print(f"Random agent scored: {state.score}")
        break
Characteristics:
  • Simple baseline for comparison
  • Explores the state space uniformly
  • Average survival: ~10-20 steps
  • Good for stress-testing edge cases

GreedyAgent

class GreedyAgent:
    def __init__(self, seed=None)
Minimizes Manhattan distance to food. Chooses moves that get the snake closer to the food target.
seed
int | None
default:"None"
Random seed for tie-breaking when multiple actions have equal distance

Implementation

def act(self, state: SnakeState, legal: list[Action]) -> Action:
    fx, fy = state.food
    best_dist = float("inf")
    best_actions: list[Action] = []
    for a in legal:
        dx, dy = DIR_DELTA[a]
        nx, ny = state.head[0] + dx, state.head[1] + dy
        dist = abs(nx - fx) + abs(ny - fy)
        if dist < best_dist:
            best_dist = dist
            best_actions = [a]
        elif dist == best_dist:
            best_actions.append(a)
    return self.rng.choice(best_actions)

Algorithm

  1. For each legal action, compute resulting position
  2. Calculate Manhattan distance: |x - food_x| + |y - food_y|
  3. Collect all actions with minimum distance
  4. Randomly break ties

Usage Example

from game_grammar import SnakeGame, GreedyAgent

game = SnakeGame(width=10, height=10, seed=42)
agent = GreedyAgent(seed=42)

state = game.reset()
for _ in range(1000):
    legal = game.legal_actions(state)
    action = agent.act(state, legal)
    state, events, done = game.step(action)
    if done:
        print(f"Greedy agent scored: {state.score}")
        break
Characteristics:
  • Goal-directed behavior
  • Often gets trapped by its own body
  • Average survival: ~30-50 steps
  • Good at short-term food collection
  • Fails at long-term planning

WallFollowerAgent

class WallFollowerAgent:
    def __init__(self, width=10, height=10, seed=None)
Prefers moves that keep the snake near walls. This creates distinctive spiral and perimeter-following patterns.
width
int
default:"10"
Grid width (must match game dimensions)
height
int
default:"10"
Grid height (must match game dimensions)
seed
int | None
default:"None"
Random seed for tie-breaking

Implementation

def _near_wall(self, x: int, y: int) -> bool:
    return x <= 0 or x >= self.width - 1 or y <= 0 or y >= self.height - 1

def act(self, state: SnakeState, legal: list[Action]) -> Action:
    wall_moves: list[Action] = []
    safe_moves: list[Action] = []
    for a in legal:
        dx, dy = DIR_DELTA[a]
        nx, ny = state.head[0] + dx, state.head[1] + dy
        # Skip moves that hit walls
        if nx < 0 or nx >= self.width or ny < 0 or ny >= self.height:
            continue
        safe_moves.append(a)
        if self._near_wall(nx, ny):
            wall_moves.append(a)
    choices = wall_moves if wall_moves else (safe_moves if safe_moves else legal)
    return self.rng.choice(choices)

Algorithm

  1. Categorize legal actions:
    • Wall moves: Result in position at edge (x/y = 0 or width/height - 1)
    • Safe moves: Don’t hit walls but may not be near walls
  2. Prefer wall moves, fall back to safe moves, last resort is any legal move
  3. Randomly choose from preferred category

Usage Example

from game_grammar import SnakeGame, WallFollowerAgent

game = SnakeGame(width=10, height=10, seed=42)
agent = WallFollowerAgent(width=10, height=10, seed=42)

state = game.reset()
for _ in range(1000):
    legal = game.legal_actions(state)
    action = agent.act(state, legal)
    state, events, done = game.step(action)
    if done:
        print(f"Wall follower scored: {state.score}")
        break
Characteristics:
  • Distinctive movement patterns
  • Follows perimeter and creates spirals
  • Average survival: ~40-70 steps
  • Good spatial coverage
  • Creates visually interesting replays

Comparing Agents

Typical performance metrics on a 10x10 grid:
AgentAvg ScoreAvg StepsMax Score
RandomAgent2-315-205-8
GreedyAgent4-635-5012-18
WallFollowerAgent6-850-7020-30
Performance varies significantly with grid size and random seed. Larger grids favor WallFollowerAgent.

Multi-Agent Comparison

Run multiple agents in parallel to compare strategies:
from game_grammar import SnakeGame, RandomAgent, GreedyAgent, WallFollowerAgent

def run_agent(agent_class, name, trials=100):
    scores = []
    for i in range(trials):
        game = SnakeGame(width=10, height=10, seed=i)
        agent = agent_class(seed=i) if agent_class != WallFollowerAgent else \
                WallFollowerAgent(width=10, height=10, seed=i)
        
        state = game.reset()
        while state.alive:
            legal = game.legal_actions(state)
            action = agent.act(state, legal)
            state, events, done = game.step(action)
            if done:
                break
        scores.append(state.score)
    
    avg_score = sum(scores) / len(scores)
    max_score = max(scores)
    print(f"{name}: avg={avg_score:.2f}, max={max_score}")

run_agent(RandomAgent, "Random")
run_agent(GreedyAgent, "Greedy")
run_agent(WallFollowerAgent, "WallFollower")
Example Output:
Random: avg=2.34, max=7
Greedy: avg=5.12, max=15
WallFollower: avg=7.45, max=28

Build docs developers (and LLMs) love