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
List of legal actions (from game.legal_actions(state))
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.
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.
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
- For each legal action, compute resulting position
- Calculate Manhattan distance:
|x - food_x| + |y - food_y|
- Collect all actions with minimum distance
- 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.
Grid width (must match game dimensions)
Grid height (must match game dimensions)
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
- 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
- Prefer wall moves, fall back to safe moves, last resort is any legal move
- 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
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