A Python library for 2D sprite-based game development.
uv add arcengine
# or
pip install arcengineARCBaseGame is the base class for ARCEngine games. Create a game by subclassing it and overriding step():
from arcengine import ARCBaseGame, ActionInput, Camera, GameAction, Level, Sprite
class MyGame(ARCBaseGame):
def step(self) -> None:
# Your game logic here.
# Call complete_action() when you are done handling the input.
self.complete_action()
level = Level([Sprite([[1]], name="player")])
# Camera is optional (defaults to 64x64).
game = MyGame(game_id="my_game", levels=[level], camera=Camera())
# Multiple frames are returned if an animation was played as a result of the action
frames = game.perform_action(ActionInput(id=GameAction.ACTION1))Base class for games. Subclass this and implement step().
current_level(Level): The current active levelcamera(Camera): The game's cameragame_id(str): The game's identifier (should be set by subclasses)action(ActionInput): The current action being performedlevel_index: int- current level index
__init__(game_id, levels, camera=None, debug=False, win_score=1, available_actions=[1,2,3,4,5,6], seed=0)
Initialize a new game.
game_id: Game identifierlevels: List of levels to initialize the game with (each level is cloned)camera: Optional camera to use. If not provided, a default 64x64 camera is createddebug: Enable debug loggingavailable_actions: List of numeric action IDsseed: Optional seed value for game logic
Raises ValueError if levels is empty.
Print a debug message if debug mode is enabled.
message: Message to print
Set the current level by index.
index: The index of the level to set as current- Raises
IndexErrorif index is out of range
Set the current level by name.
name: The level name to match- Raises
ValueErrorif no level matches
Perform an action and return the resulting frame data.
This method runs step() in a loop until complete_action() is called, rendering each frame. It should not be overridden; implement game logic in step().
action_input: The action to performraw: If True, returnsFrameDataRawwith numpy frames- Returns:
FrameDataorFrameDataRaw - Raises
ValueErrorif an action exceeds 1000 frames
Mark the current action as complete. Call this when the provided action is fully resolved.
Check if the current action is complete.
- Returns: True if the action is complete, False otherwise
Call this when the player has beaten the game.
Call this when the player has lost the game.
Handle RESET actions, choosing between level_reset() and full_reset() based on action count and ONLY_RESET_LEVELS.
Reset the entire game back to its initial state.
Reset only the current level back to its initial state.
Step the game. This is where your game logic should be implemented.
REQUIRED: Call complete_action() when the action is complete. It does not need to be called every step, but once the action is complete. The engine will keep calling step() and rendering frames until the action is complete.
Try to move a sprite and return a list of sprites it collides with.
This method attempts to move the sprite by the given deltas and checks for collisions. If any collisions are detected, the sprite is not moved and the method returns a list of collided sprites.
sprite_name: The name of the sprite to movedx: The change in x position (positive = right, negative = left)dy: The change in y position (positive = down, negative = up)- Returns: A list of sprites collided with. If no collisions occur, the sprite is moved and an empty list is returned
- Raises
ValueErrorif no sprite with the given name is found
Example (try_move):
# Try to move a sprite right by 1 pixel
collisions = game.try_move("player", 1, 0)
if not collisions:
print("Move successful!")
else:
print(f"Collided with: {[sprite.name for sprite in collisions]}")Try to move a specific sprite and return a list of sprites it collides with.
sprite: The sprite to movedx: The change in x position (positive = right, negative = left)dy: The change in y position (positive = down, negative = up)- Returns: A list of sprites collided with. If no collisions occur, the sprite is moved and an empty list is returned
Advance to the next level or mark the game as won if the last level is complete.
Hook called when the level is set. Override to apply level-specific setup.
level: The level being set
Get the camera pixels at a sprite's location.
sprite: The sprite to sample- Returns: A numpy array of pixels covering the sprite's area
Get the camera pixels at a given position.
x,y: Top-left position in camera spacewidth,height: Dimensions of the sample area- Returns: A numpy array of pixels for the given region
perform_actionrunsstep()in a loop untilcomplete_action()is called. It raisesValueErrorif an action exceeds 1000 frames.
The public API is exported from arcengine.__init__:
Import necessary components from arcengine:
from arcengine import (
ARCBaseGame,
Camera,
Level,
)Enum of available actions with attached data model types.
RESET(id 0) usesSimpleAction.ACTION1-ACTION5andACTION7useSimpleAction.ACTION6usesComplexActionto encode screen coordinatesxandy(0,0 is the top left pixel). Used for click inputs
Common client/UI conventions:
ACTION1: Up or W or 1ACTION2: Down or S or 2ACTION3: Left or A or 3ACTION4: Right or D or 4ACTION5: SpacebarACTION7: Z - Used for Undo
A 2D sprite with position, rotation, scale, and collision behavior.
from arcengine import Sprite, BlockingMode, InteractionMode
# Create a simple 2x2 sprite
sprite_simple = Sprite([
[1, 2],
[3, 4]
])
# Create a sprite with custom properties
sprite_custom = Sprite(
pixels=[[1, 2], [3, 4]],
name="player",
x=10,
y=20,
layer=1,
scale=2,
rotation=90,
mirror_ud=False,
mirror_lr=False,
blocking=BlockingMode.PIXEL_PERFECT,
interaction=InteractionMode.TANGIBLE,
# If interaction is None, visible/collidable determine the mode.
# visible=True, collidable=True are the defaults.
tags=["player"],
)-
Pixels are palette indices. Any negative value is treated as transparent when rendering. -1 is treated as transparent and not blocking/non-colliding (applies to BlockingMode.PIXEL_PERFECT) while other negative values are considered blocking.
-
Rotation is limited to
0,90,180,270degrees. -
scale:- Positive values upscale (2 = double size, 3 = triple size).
- Negative values downscale by a divisor:
-1=> divide by 2,-2=> divide by 3, etc. 0is invalid and raisesValueError.
-
If
interactionisNone,visibleandcollidabledetermine the interaction mode.
name: strx: int,y: intlayer: intscale: introtation: intblocking: BlockingModepixels: np.ndarrayinteraction: InteractionModetags: list[str]mirror_ud: bool,mirror_lr: boolis_visible: boolis_collidable: boolwidth: int,height: int(based on rendered size)
__init__(pixels, name=None, x=0, y=0, layer=0, scale=1, rotation=0, mirror_ud=False, mirror_lr=False, blocking=BlockingMode.PIXEL_PERFECT, interaction=None, visible=True, collidable=True, tags=[])
Initialize a new Sprite.
pixels: 2D list or 2D numpy array representing the sprite's pixelsname: Optional sprite name (default: generates UUID)x: X coordinate in pixels (default: 0)y: Y coordinate in pixels (default: 0)layer: Z-order layer for rendering (default: 0, higher values render on top)scale: Scale factor (default: 1)rotation: Rotation in degrees (default: 0)mirror_ud,mirror_lr: Optional vertical/horizontal mirroringblocking: Collision detection method (default: PIXEL_PERFECT)interaction: Optional interaction mode override. IfNone,visible/collidabledetermine the modevisible,collidable: Used only wheninteractionisNonetags: Optional list of string tags
Raises ValueError if scale is 0, pixels is not a 2D list/array, rotation is invalid,
or if downscaling factor doesn't evenly divide sprite dimensions.
Create an independent copy of this sprite.
new_name: Optional name for the cloned sprite (default: reuses current name)- Returns: A new Sprite instance with the same properties but independent state
Set the sprite's position.
x: New X coordinate in pixelsy: New Y coordinate in pixels
Move the sprite by the given deltas.
dx: Change in x position (positive = right, negative = left)dy: Change in y position (positive = down, negative = up)
Set the sprite's scale factor.
scale: The new scale factor:- Positive values scale up (2 = double size, 3 = triple size)
- Negative values scale down (-1 = half size, -2 = one-third size, -3 = one-fourth size)
- Zero is invalid
- Raises
ValueErrorif scale is 0 or if downscaling factor doesn't evenly divide sprite dimensions
For example:
sprite = Sprite([[1, 2], [3, 4]])
# Upscaling examples
sprite.set_scale(2) # Doubles size in both dimensions
sprite.set_scale(3) # Triples size in both dimensions
# Downscaling examples
sprite.set_scale(-1) # Half size (divide dimensions by 2)
sprite.set_scale(-2) # One-third size (divide dimensions by 3)
sprite.set_scale(-3) # One-fourth size (divide dimensions by 4)Adjust the sprite's scale by a delta value, moving one step at a time.
The method will adjust the scale by incrementing or decrementing by 1 repeatedly until reaching the target scale. This ensures smooth transitions and validates each step.
Negative scales indicate downscaling factors:
- scale = -1: half size (divide by 2)
- scale = -2: one-third size (divide by 3)
- scale = -3: one-fourth size (divide by 4)
Examples:
- Current scale 1, delta +2 -> Steps through: 1 -> 2 -> 3
- Current scale 1, delta -2 -> Steps through: 1 -> 0 -> -1 (half size)
- Current scale -2, delta +3 -> Steps through: -2 -> -1 -> 0 -> 1
Raises ValueError if any intermediate scale would be 0 or if a downscaling factor doesn't evenly divide sprite dimensions.
Set the sprite's rotation to a specific value.
rotation: The new rotation in degrees (must be 0, 90, 180, or 270)- Raises
ValueErrorif rotation is not a valid 90-degree increment
Rotate the sprite by a given amount.
delta: The change in rotation in degrees (must result in a valid rotation)- Raises
ValueErrorif resulting rotation is not a valid 90-degree increment
Set the sprite's blocking behavior.
blocking: The new blocking behavior (BlockingMode enum value)- Raises
ValueErrorif blocking is not a BlockingMode enum value
Set the sprite's interaction mode.
interaction: The new interaction mode (InteractionMode enum value)- Raises
ValueErrorif interaction is not an InteractionMode enum value
Set the sprite's visibility.
visible: The new visibility state
Set the sprite's collidable state.
collidable: The new collidable state
Set the sprite's rendering layer.
layer: New layer value. Higher values render on top.
Set the sprite's mirror up/down state.
mirror_ud: True to flip vertically
Set the sprite's mirror left/right state.
mirror_lr: True to flip horizontally
Set the sprite's name.
name: New name for the sprite- Raises
ValueErrorif name is empty
Render the sprite with current scale and rotation.
- Returns: A 2D numpy array representing the rendered sprite
- Raises
ValueErrorif downscaling factor doesn't evenly divide the sprite dimensions
Check if this sprite collides with another sprite.
The collision check follows these rules:
- A sprite cannot collide with itself
- Non-collidable sprites (based on interaction mode) never collide (unless
ignoreMode=True) - For collidable sprites, the collision detection method is based on their blocking mode:
- NOT_BLOCKED: Always returns False
- BOUNDING_BOX: Simple rectangular collision check
- PIXEL_PERFECT: Precise pixel-level collision detection
other: The other sprite to check collision withignoreMode: If True, bypasses interaction and blocking checks- Returns: True if the sprites collide, False otherwise
Remap the sprite's color.
old_color: The old color to remap, or None to remap all colorsnew_color: The new color to remap to
Merge two sprites together.
This method creates a new sprite that combines the pixels of both sprites. When pixels overlap, the non -1 pixels are prioritized
other: The other sprite to merge with- Returns: A new Sprite instance containing the merged pixels
An enumeration defining different collision detection behaviors for sprites:
NOT_BLOCKED: No collision detectionBOUNDING_BOX: Collision detection using the sprite's bounding boxPIXEL_PERFECT: Collision detection using pixel-perfect testing
An enumeration defining how a sprite interacts with the game world:
TANGIBLE: Visible and can be collided withINTANGIBLE: Visible but cannot be collided with (ghost-like)INVISIBLE: Not visible but can be collided with (invisible wall)REMOVED: Not visible and cannot be collided with (effectively removed)
Defines the viewport and renders sprites to a 64x64 output.
from arcengine import Camera
# Create a default camera (64x64 viewport)
camera = Camera()
# Create a custom camera
camera = Camera(
x=10, # X position in pixels
y=20, # Y position in pixels
width=32, # Viewport width (max 64)
height=32, # Viewport height (max 64)
background=1, # Background color index
letter_box=2, # Letter box color index
interfaces=[], # Optional list of renderable interfaces
)- Output is always 64x64. The camera view is uniformly upscaled (nearest neighbor) and
letterboxed with
letter_boxcolor as needed. - The scale factor is
min(64 // width, 64 // height). interfacesis an optional list ofRenderableUserDisplayoverlays.
x: int,y: intwidth: int,height: int(max 64)background: int,letter_box: int
Initialize a new Camera.
Args:
x(int): X coordinate in pixels (default: 0)y(int): Y coordinate in pixels (default: 0)width(int): Viewport width in pixels (default: 64, max: 64)height(int): Viewport height in pixels (default: 64, max: 64)background(int): Background color index (default: 5 - Black)letter_box(int): Letter box color index (default: 5 - Black)interfaces(list[RenderableUserDisplay]): Optional list of renderable interfaces to initialize with
Raises:
ValueError: If width or height exceed 64 pixels or are negative
Move the camera by the specified delta.
dx: Change in x positiondy: Change in y position
Resize the camera viewport.
width,height: New dimensions (max 64)
Render the camera view.
The rendered output is always 64x64 pixels. If the camera's viewport is smaller, the view is scaled up uniformly (nearest neighbor) to fit within 64x64, and the remaining space is filled with the letter_box color.
Args:
sprites(list[Sprite]): List of sprites to render
Returns:
np.ndarray: The rendered view as a 64x64 numpy array
Replace the current interfaces with new ones. This method replaces all current interfaces with the provided ones and stores them as-is (no cloning).
Args:
new_interfaces(list[RenderableUserDisplay]): List of new interfaces to use. These should be cloned before passing them in.
Convert display coordinates (64x64) to camera grid coordinates.
display_x,display_y: Display-space coordinates (0-63)- Returns:
(x, y)grid coordinates, orNoneif the point lies in the letterbox area
The RenderableUserDisplay class is an abstract base class that defines the interface for UI elements that can be rendered by the camera. It is used as the final step in the camera's rendering pipeline to produce the 64x64 output frame.
import numpy as np
from arcengine import RenderableUserDisplay, Sprite
class MyUI(RenderableUserDisplay):
def render_interface(self, frame: np.ndarray) -> np.ndarray:
# Modify the frame in-place and return it
return frameRender this UI element onto the given frame.
frame: The 64x64 numpy array to render onto- Returns: The modified frame (implementations should modify in-place)
Helper to draw a sprite onto a frame with clipping.
frame: The 64x64 numpy array to draw ontosprite: The sprite to drawstart_x,start_y: Top-left position in frame coordinates- Returns: The modified frame
The ToggleableUserDisplay class is an example implementation of RenderableUserDisplay that manages a collection of sprite pairs (enabled/disabled states) and provides methods to toggle between them.
from arcengine import ToggleableUserDisplay, Sprite
# Create a toggleable UI element with sprite pairs
ui_element = ToggleableUserDisplay([
(enabled_sprite1, disabled_sprite1),
(enabled_sprite2, disabled_sprite2)
])
# Enable/disable specific sprite pairs
ui_element.enable(0) # Enable first pair
ui_element.disable(1) # Disable second pair
# Check if a pair is enabled
is_enabled = ui_element.is_enabled(0)Initialize a new ToggleableUserDisplay.
sprite_pairs: List of(enabled_sprite, disabled_sprite)tuples. Each sprite is cloned.
Create a deep copy of this UI element.
- Returns: A new ToggleableUserDisplay instance with cloned sprite pairs
Check if a sprite pair is enabled.
index: Index of the sprite pair to check- Returns: True if the pair is enabled, False otherwise
- Raises
ValueErrorif index is out of range
Enable a sprite pair.
index: Index of the sprite pair to enable- Raises
ValueErrorif index is out of range
Disable a sprite pair.
index: Index of the sprite pair to disable- Raises
ValueErrorif index is out of range
Enable all sprite pairs that have the given tag.
tag: Tag to search for
Disable all sprite pairs that have the given tag.
tag: Tag to search for
Enable the first disabled sprite pair with the given tag.
tag: Tag to search for- Returns: True if a pair was enabled, False otherwise
Disable the first enabled sprite pair with the given tag.
tag: Tag to search for- Returns: True if a pair was disabled, False otherwise
Render the UI element onto the given frame.
frame: The 64x64 numpy array to render onto
This method renders all sprite pairs, using the enabled sprite if the pair is enabled, and the disabled sprite if the pair is disabled.
Manages a collection of sprites and level metadata.
from arcengine import Level, Sprite, PlaceableArea
sprites = [
Sprite([[1]], name="player"),
Sprite([[2]], name="enemy")
]
# Create an empty level
level_empty = Level()
# Create a level with initial sprites
level = Level(
sprites=sprites,
grid_size=(16, 16),
data={"difficulty": "easy"},
name="level_1",
)name: strgrid_size: tuple[int, int] | None
Initialize a new Level.
sprites: Optional list of sprites to initialize the level withgrid_size: Optional(width, height)tuple for grid sizingdata: Optional metadata dictionaryname: Level name
Add a sprite to the level.
sprite: The sprite to add
Remove a sprite from the level.
sprite: The sprite to remove
Remove all sprites from the level.
Get all sprites in the level.
- Returns: A copy of the sprite list
Get all sprites with the given name.
name: The name to search for- Returns: List of sprites with the given name
Get all sprites that have the given tag.
tag: The tag to search for- Returns: List of sprites that have the tag
Get all sprites that have all of the given tags (AND).
tags: Tags to search for- Returns: List of sprites with all tags
Get all sprites that have any of the specified tags (OR).
tags: Tags to search for- Returns: List of sprites that have any tag
Get all unique tags from all sprites in the level.
- Returns: A set of tag strings
Get the top-most sprite at the given coordinates.
x,y: Coordinates to searchtag: Optional tag filterignore_collidable: If True, includes non-collidable sprites- Returns: The first matching sprite or
None
Return all sprites in the level that collide with the given sprite.
sprite: The sprite to check for collisionsignoreMode: If True, bypasses interaction/blocking checks
Get metadata by key.
- Returns: The stored value or
None
Create a deep copy of this level.
- Returns: A new
Levelinstance with cloned sprites
To set up the development environment:
-
Clone the repository:
git clone git@github.com:arcprize/ARCEngine.git cd ARCEngine -
Create and activate a virtual environment using uv:
uv venv source .venv/bin/activate # On Windows: .venv\Scripts\activate
-
Install development dependencies:
uv sync
-
Install git hooks:
pre-commit install
This repo uses ruff to lint/format and mypy for type checking:
pre-commit run --all-filesNote: by default these tools run automatically before git commit. It's also recommended
to set up ruff inside your IDE (https://docs.astral.sh/ruff/editors/setup/).
This project does not accept external contributions.
If you use this project in your research, please cite it as:
@software{arc_agi,
author = {ARC Prize Foundation},
title = {ARC Game Engine},
year = {2026},
url = {https://github.com/arcprize/ARCEngine},
version = {0.9.3}
}MIT License
Copyright (c) 2026 ARC Prize Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.