Skip to main content
This example walks through a simple game where randomly generated sprites appear and the player must click to remove them all to win. See the full code

Game File Structure

Each game adheres to the following structure:
  • Imports
    Import ARCEngine classes and any standard library modules needed.
  • Sprite Definitions
    Define sprite templates in a dictionary. Sprites are objects with a name, rectangular pixel array, and optional tags.
  • Level Definitions
    Define levels as a list of Level objects, each containing sprite placements and configuration data.
  • Constants
    Define game-wide constants for colors, grid size, and gameplay parameters.
  • Game Class
    Create a class extending ARCBaseGame that implements game logic. The class name must match the 4-character game ID with the first letter capitalized.

Directory Structure

To start building your own environment, create the following directory structure:
ARC-AGI/
└── environment_files/
    └── ab12/               # use any game ID you want
        └── v1/             # use any version identifier you want
            ├── ab12.py     # must match game ID above
            └── metadata.json

Metadata File

Create metadata.json:
{
  "game_id": "ab12-v1",
  "local_dir": "environment_files\\ab12\\v1"
}
FieldDescription
game_idUnique identifier in format {4-char game ID}-{version}
default_fpsFrames per second for playback (optional)
baseline_actionsArray of average action counts per level (optional)
tagsOptional tags for categorization
local_dirRelative path to game directory

Imports and Constants

Start with the imports and constants:
import random

from arcengine import (
    ARCBaseGame,
    Camera,
    GameAction,
    Level,
    Sprite,
)

BACKGROUND_COLOR = 0  
PADDING_COLOR = 4     

# Available colors for sprites (excluding all black -> white colors for visibility)
SPRITE_COLORS = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

# Parameters for Procedural Generation
MIN_SIZE = 1
MAX_SIZE = 4

Sprite and Level Definitions

Define the sprites and levels for your game.
In this example, the base sprite template that will be cloned and modified during generation:
# Base sprite template - single pixel will be cloned, scaled, and recolored
sprites = {
    "sprite-1": Sprite(
        pixels=[
            [9],
        ],
        name="sprite-1",
        visible=True,
        collidable=True,
    ),
}

# Create levels array with all level definitions
levels = [
    # Level 1
    Level(
        sprites=[],
        grid_size=(8, 8),
    ),
    Level(
        sprites=[],
        grid_size=(16, 16),
    ),
    Level(
        sprites=[],
        grid_size=(24, 24),
    ),
    Level(
        sprites=[],
        grid_size=(32, 32),
    ),
    Level(
        sprites=[],
        grid_size=(64, 64),
    ),
]

Game Class Definition

The game class extends ARCBaseGame and accepts a seed parameter:
class Ab12(ARCBaseGame):
    """Click-to-remove game with seeded random sprite generation."""

    def __init__(self, seed: int = 0) -> None:
        self._rng = random.Random(seed)
    
        # Create camera with background and padding colors
        camera = Camera(
            background=BACKGROUND_COLOR,
            letter_box=PADDING_COLOR,
            width=8,
            height=8,
        )

        # Initialize base game
        super().__init__(
            game_id="ab12",
            levels=levels,
            camera=camera,
        )

Gameplay Logic and Mechanics

This game generates sprites by cloning from the sprite dictionary and applying random properties:
    def generate_sprites(self) -> None:
           """Generate a random set of sprites based on the seed."""
           # Determine number of sprites
           cell_count = self.current_level.grid_size[0] * self.current_level.grid_size[1]
           sprite_count = cell_count // 64  
           for idx in range(sprite_count):
               scale = self._rng.randint(MIN_SIZE, MAX_SIZE)
               color = self._rng.choice(SPRITE_COLORS)
               x = self._rng.randint(0, self.current_level.grid_size[0] - 1)
               y = self._rng.randint(0, self.current_level.grid_size[1] - 1)
               # Create the sprite setting color, scale, and position then add it to the level
               sprite = sprites[f"sprite-1"].clone().color_remap(None, color).set_scale(scale).set_position(x, y)
               self.current_level.add_sprite(sprite)

Generation Pattern

The method chains operations on the cloned sprite:
  1. .clone() - Creates independent copy of template
  2. .color_remap(None, color) - Changes all pixels to random color
  3. .set_scale(scale) - Applies the random scale
  4. .set_position(x, y) - Places at random position in the grid
These can be used in level definitions directly for static levels.

Level Initialization

The on_set_level() method is called when a level loads:
    def on_set_level(self, level: Level) -> None:
        """Called when the level is set."""
        # Generate sprites based on seed
        self.generate_sprites()
This method triggers sprite generation each time the level is set or reset.

Win Condition

Define when the player wins:
    def _check_win(self) -> bool:
        """Check if all targets have been removed."""
        return len(self.current_level._sprites) == 0

Game Loop

The step() method contains the main game logic.

Action Flow

  1. Check action type: GameAction.ACTION6 is the click action
  2. Get coordinates: Extract x, y from action.data
  3. Convert coordinates: display_to_grid() handles camera scaling
  4. Find sprite: Check if click hit any sprite
  5. Remove sprite: Update level state
  6. Check win: Advance to the next level or win the game on the last level if condition is met
  7. Complete action: Always call self.complete_action()
    def step(self) -> None:
        """Process game logic for each step."""

        # 1. Check action type - GameAction.ACTION6 is the click action
        if self.action.id == GameAction.ACTION6:
            # 2. Get coordinates - Extract x, y from action.data
            x = self.action.data.get("x", 0)
            y = self.action.data.get("y", 0)

            # 3. Convert coordinates - display_to_grid() handles camera scaling
            coords = self.camera.display_to_grid(x, y)

            if coords:
                grid_x, grid_y = coords

                # 4. Find sprite - Check if click hit any sprite
                clicked_sprite = self.current_level.get_sprite_at(grid_x, grid_y)
                if clicked_sprite:
                    # 5. Remove sprite - Update level state
                    self.current_level.remove_sprite(clicked_sprite)

                    # 6. Check win - Advance to next level if condition is met
                    if self._check_win():
                        self.next_level()

        # 7. Complete action - Always call self.complete_action()
        self.complete_action()

Testing the Game

Run the game using the ARC-AGI-3 client:
import arc_agi
from arcengine import GameAction

# Default: looks for games in "environment_files" directory
arc = arc_agi.Arcade()
env = arc.make("ab12-v1", seed=0, render_mode="terminal")

# Or specify a custom directory
arc = arc_agi.Arcade(environments_dir="./my_games")
env = arc.make("ab12-v1", seed=0, render_mode="terminal")

# Perform clicks (ACTION6 with x, y coordinates)
env.step(GameAction.ACTION6, data={"x": 32, "y": 32})
If you’d like to see a 100-step run on your game, play the game with the Sample Agent.

Complete Code

import random

from arcengine import (
    ARCBaseGame,
    Camera,
    GameAction,
    Level,
    Sprite,
)

# Create sprites dictionary with all sprite definitions
sprites = {
    "sprite-1": Sprite(
        pixels=[
            [9],
        ],
        name="sprite-1",
        visible=True,
        collidable=True,
    ),
}

# Create levels array with all level definitions
levels = [
    # Level 1
    Level(
        sprites=[],
        grid_size=(8, 8),
    ),
    Level(
        sprites=[],
        grid_size=(16, 16),
    ),
    Level(
        sprites=[],
        grid_size=(24, 24),
    ),
    Level(
        sprites=[],
        grid_size=(32, 32),
    ),
    Level(
        sprites=[],
        grid_size=(64, 64),
    ),
]

BACKGROUND_COLOR = 0
PADDING_COLOR = 4

# Available colors for sprites (excluding all black -> white colors for visibility)
SPRITE_COLORS = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

# Parameters for Procedural Generation
MIN_SIZE = 1
MAX_SIZE = 4


class Ab12(ARCBaseGame):
    """Click-to-remove puzzle game with seeded random sprite generation."""

    def __init__(self, seed: int = 0) -> None:
        self._rng = random.Random(seed)
        # Create camera with background and padding colors
        camera = Camera(
            background=BACKGROUND_COLOR,
            letter_box=PADDING_COLOR,
            width=8,
            height=8,
        )

        super().__init__(
            game_id="ab12",
            levels=levels,
            camera=camera,
        )

    def generate_sprites(self) -> None:
           """Generate a random set of sprites based on the seed."""
           # Determine number of sprites
           cell_count = self.current_level.grid_size[0] * self.current_level.grid_size[1]
           sprite_count = cell_count // 64  
           for idx in range(sprite_count):
               scale = self._rng.randint(MIN_SIZE, MAX_SIZE)
               color = self._rng.choice(SPRITE_COLORS)
               x = self._rng.randint(0, self.current_level.grid_size[0] - 1)
               y = self._rng.randint(0, self.current_level.grid_size[1] - 1)
               # Create the sprite setting color, scale, and position then add it to the level
               sprite = sprites[f"sprite-1"].clone().color_remap(None, color).set_scale(scale).set_position(x, y)
               self.current_level.add_sprite(sprite)

    def on_set_level(self, level: Level) -> None:
        """Called when the level is set."""
        # Generate sprites based on seed
        self.generate_sprites()

    def _check_win(self) -> bool:
        """Check if all targets have been removed."""
        return len(self.current_level._sprites) == 0

    def step(self) -> None:
        """Process game logic for each step."""
        # Handle click action (ACTION6)
        if self.action.id == GameAction.ACTION6:
            x = self.action.data.get("x", 0)
            y = self.action.data.get("y", 0)

            # Convert display coordinates to grid coordinates
            coords = self.camera.display_to_grid(x, y)

            if coords:
                grid_x, grid_y = coords

                # Find and remove the clicked sprite from the level
                clicked_sprite = self.current_level.get_sprite_at(grid_x, grid_y)
                if clicked_sprite:
                    self.current_level.remove_sprite(clicked_sprite)

                    # Check win condition
                    if self._check_win():
                        self.next_level()

        self.complete_action()

Further Reading

For more information, refer to the ARC Engine Documentation.