Metadata-Version: 2.3
Name: arcengine
Version: 0.9.3
Summary: A Python library using numpy
Author: arcprizefoundation
Author-email: arcprizefoundation <>
License: 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.
Requires-Dist: numpy>=1.24.0
Requires-Dist: pydantic>=2.10.6
Requires-Dist: mypy>=1.15.0 ; extra == 'dev'
Requires-Dist: pre-commit>=4.2.0 ; extra == 'dev'
Requires-Dist: ruff>=0.11.4 ; extra == 'dev'
Requires-Python: >=3.12
Provides-Extra: dev
Description-Content-Type: text/markdown

# ARCEngine

A Python library for 2D sprite-based game development.

## Installation

Add ARCEngine to your project using one of these methods:

### Using uv (recommended)

Add ARCEngine to your `pyproject.toml`:

```toml
[project]
dependencies = [
    "arcengine @ git+ssh://git@github.com/arcprize/ARCEngine.git@main",
]
```

Then install with uv:

```bash
uv sync
```

## Quick Start

`ARCBaseGame` is the base class for ARCEngine games. Create a game by subclassing it and overriding `step()`:

```python
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))
```

## API Documentation

### `ARCBaseGame`
Base class for games. Subclass this and implement `step()`.

#### Properties

- `current_level` (Level): The current active level
- `camera` (Camera): The game's camera
- `game_id` (str): The game's identifier (should be set by subclasses)
- `action` (ActionInput): The current action being performed
- `level_index: int` - current level index

#### Methods

##### `__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 identifier
- `levels`: 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 created
- `debug`: Enable debug logging
- `available_actions`: List of numeric action IDs
- `seed`: Optional seed value for game logic

Raises `ValueError` if `levels` is empty.

##### `debug(message)`
Print a debug message if debug mode is enabled.

- `message`: Message to print

##### `set_level(index)`
Set the current level by index.

- `index`: The index of the level to set as current
- Raises `IndexError` if index is out of range

##### `set_level_by_name(name)`
Set the current level by name.

- `name`: The level name to match
- Raises `ValueError` if no level matches

##### `perform_action(action_input, raw=False)`
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 perform
- `raw`: If True, returns `FrameDataRaw` with numpy frames
- Returns: `FrameData` or `FrameDataRaw`
- Raises `ValueError` if an action exceeds 1000 frames

##### `complete_action()`
Mark the current action as complete. Call this when the provided action is fully resolved.

##### `is_action_complete()`
Check if the current action is complete.

- Returns: True if the action is complete, False otherwise

##### `win()`
Call this when the player has beaten the game.

##### `lose()`
Call this when the player has lost the game.

##### `handle_reset()`
Handle RESET actions, choosing between `level_reset()` and `full_reset()` based on action count and `ONLY_RESET_LEVELS`.

##### `full_reset()`
Reset the entire game back to its initial state.

##### `level_reset()`
Reset only the current level back to its initial state.

##### `step()`
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_move(sprite_name, dx, dy)`
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 move
- `dx`: 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 `ValueError` if no sprite with the given name is found

Example (`try_move`):

```python
# 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_move_sprite(sprite, dx, dy)`
Try to move a specific sprite and return a list of sprites it collides with.

- `sprite`: The sprite to move
- `dx`: 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

##### `next_level()`
Advance to the next level or mark the game as won if the last level is complete.

##### `on_set_level(level)`
Hook called when the level is set. Override to apply level-specific setup.

- `level`: The level being set

##### `get_pixels_at_sprite(sprite)`
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_pixels(x, y, width, height)`
Get the camera pixels at a given position.

- `x`, `y`: Top-left position in camera space
- `width`, `height`: Dimensions of the sample area
- Returns: A numpy array of pixels for the given region

#### Notes

- `perform_action` runs `step()` in a loop until `complete_action()` is called. It raises
  `ValueError` if an action exceeds 1000 frames.


## API

The public API is exported from `arcengine.__init__`:

Import necessary components from arcengine:

```python
from arcengine import (
    ARCBaseGame,
    Camera,
    Level,
)
```

#### `GameAction`
Enum of available actions with attached data model types.

- `RESET` (id 0) uses `SimpleAction`.
- `ACTION1`-`ACTION5` and `ACTION7` use `SimpleAction`.
- `ACTION6` uses `ComplexAction` to encode screen coordinates `x` and `y` (0,0 is the top left pixel). Used for click inputs

Common client/UI conventions:
- `ACTION1`: Up or W or 1
- `ACTION2`: Down or S or 2
- `ACTION3`: Left or A or 3
- `ACTION4`: Right or D or 4
- `ACTION5`: Spacebar
- `ACTION7`: Z - Used for Undo

### `Sprite`
A 2D sprite with position, rotation, scale, and collision behavior.

```python
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"],
)
```

#### Notes

- 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`, `270` degrees.
- `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.
  - `0` is invalid and raises `ValueError`.
- If `interaction` is `None`, `visible` and `collidable` determine the interaction mode.

#### Properties

- `name: str`
- `x: int`, `y: int`
- `layer: int`
- `scale: int`
- `rotation: int`
- `blocking: BlockingMode`
- `pixels: np.ndarray`
- `interaction: InteractionMode`
- `tags: list[str]`
- `mirror_ud: bool`, `mirror_lr: bool`
- `is_visible: bool`
- `is_collidable: bool`
- `width: int`, `height: int` (based on rendered size)

#### Methods

##### `__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 pixels
- `name`: 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 mirroring
- `blocking`: Collision detection method (default: PIXEL_PERFECT)
- `interaction`: Optional interaction mode override. If `None`, `visible`/`collidable` determine the mode
- `visible`, `collidable`: Used only when `interaction` is `None`
- `tags`: 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.

##### `clone(new_name=None)`
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_position(x, y)`
Set the sprite's position.

- `x`: New X coordinate in pixels
- `y`: New Y coordinate in pixels

##### `move(dx, dy)`
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_scale(scale)`
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 `ValueError` if scale is 0 or if downscaling factor doesn't evenly divide sprite dimensions

For example:
```python
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_scale(delta)`
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_rotation(rotation)`
Set the sprite's rotation to a specific value.

- `rotation`: The new rotation in degrees (must be 0, 90, 180, or 270)
- Raises `ValueError` if rotation is not a valid 90-degree increment

##### `rotate(delta)`
Rotate the sprite by a given amount.

- `delta`: The change in rotation in degrees (must result in a valid rotation)
- Raises `ValueError` if resulting rotation is not a valid 90-degree increment

##### `set_blocking(blocking)`
Set the sprite's blocking behavior.

- `blocking`: The new blocking behavior (BlockingMode enum value)
- Raises `ValueError` if blocking is not a BlockingMode enum value

##### `set_interaction(interaction)`
Set the sprite's interaction mode.

- `interaction`: The new interaction mode (InteractionMode enum value)
- Raises `ValueError` if interaction is not an InteractionMode enum value

##### `set_visible(visible)`
Set the sprite's visibility.

- `visible`: The new visibility state

##### `set_collidable(collidable)`
Set the sprite's collidable state.

- `collidable`: The new collidable state

##### `set_layer(layer)`
Set the sprite's rendering layer.

- `layer`: New layer value. Higher values render on top.

##### `set_mirror_ud(mirror_ud)`
Set the sprite's mirror up/down state.

- `mirror_ud`: True to flip vertically

##### `set_mirror_lr(mirror_lr)`
Set the sprite's mirror left/right state.

- `mirror_lr`: True to flip horizontally

##### `set_name(name)`
Set the sprite's name.

- `name`: New name for the sprite
- Raises `ValueError` if name is empty

##### `render()`
Render the sprite with current scale and rotation.

- Returns: A 2D numpy array representing the rendered sprite
- Raises `ValueError` if downscaling factor doesn't evenly divide the sprite dimensions

##### `collides_with(other, ignoreMode=False)`
Check if this sprite collides with another sprite.

The collision check follows these rules:
1. A sprite cannot collide with itself
2. Non-collidable sprites (based on interaction mode) never collide (unless `ignoreMode=True`)
3. 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 with
- `ignoreMode`: If True, bypasses interaction and blocking checks
- Returns: True if the sprites collide, False otherwise

##### `color_remap(old_color, new_color)`
Remap the sprite's color.

- `old_color`: The old color to remap, or None to remap all colors
- `new_color`: The new color to remap to

##### `merge(other)`
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

### BlockingMode

An enumeration defining different collision detection behaviors for sprites:

- `NOT_BLOCKED`: No collision detection
- `BOUNDING_BOX`: Collision detection using the sprite's bounding box
- `PIXEL_PERFECT`: Collision detection using pixel-perfect testing

### InteractionMode

An enumeration defining how a sprite interacts with the game world:

- `TANGIBLE`: Visible and can be collided with
- `INTANGIBLE`: 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)

### `Camera`
Defines the viewport and renders sprites to a 64x64 output.

```python
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
)
```

#### Notes

- Output is always 64x64. The camera view is uniformly upscaled (nearest neighbor) and
  letterboxed with `letter_box` color as needed.
- The scale factor is `min(64 // width, 64 // height)`.
- `interfaces` is an optional list of `RenderableUserDisplay` overlays.

#### Properties

- `x: int`, `y: int`
- `width: int`, `height: int` (max 64)
- `background: int`, `letter_box: int`

#### Methods

##### `__init__(x=0, y=0, width=64, height=64, background=5, letter_box=5, interfaces=[])`

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(dx, dy)`
Move the camera by the specified delta.

- `dx`: Change in x position
- `dy`: Change in y position

##### `resize(width, height)`
Resize the camera viewport.

- `width`, `height`: New dimensions (max 64)

##### `render(sprites)`
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_interface(new_interfaces)`
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.

##### `display_to_grid(display_x, display_y)`
Convert display coordinates (64x64) to camera grid coordinates.

- `display_x`, `display_y`: Display-space coordinates (0-63)
- Returns: `(x, y)` grid coordinates, or `None` if the point lies in the letterbox area

### `RenderableUserDisplay`
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.

```python
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 frame
```

#### Methods

##### `render_interface(frame)`
Render this UI element onto the given frame.

- `frame`: The 64x64 numpy array to render onto
- Returns: The modified frame (implementations should modify in-place)

##### `draw_sprite(frame, sprite, start_x, start_y)`
Helper to draw a sprite onto a frame with clipping.

- `frame`: The 64x64 numpy array to draw onto
- `sprite`: The sprite to draw
- `start_x`, `start_y`: Top-left position in frame coordinates
- Returns: The modified frame

### ToggleableUserDisplay

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.

```python
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)

```
#### Methods

##### `__init__(sprite_pairs)`
Initialize a new ToggleableUserDisplay.

- `sprite_pairs`: List of `(enabled_sprite, disabled_sprite)` tuples. Each sprite is cloned.

##### `clone()`
Create a deep copy of this UI element.

- Returns: A new ToggleableUserDisplay instance with cloned sprite pairs

##### `is_enabled(index)`
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 `ValueError` if index is out of range

##### `enable(index)`
Enable a sprite pair.

- `index`: Index of the sprite pair to enable
- Raises `ValueError` if index is out of range

##### `disable(index)`
Disable a sprite pair.

- `index`: Index of the sprite pair to disable
- Raises `ValueError` if index is out of range

##### `enable_all_by_tag(tag)`
Enable all sprite pairs that have the given tag.

- `tag`: Tag to search for

##### `disabled_all_by_tag(tag)`
Disable all sprite pairs that have the given tag.

- `tag`: Tag to search for

##### `enable_first_by_tag(tag)`
Enable the first disabled sprite pair with the given tag.

- `tag`: Tag to search for
- Returns: True if a pair was enabled, False otherwise

##### `disabled_first_by_tag(tag)`
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_interface(frame)`
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.

### `Level`
Manages a collection of sprites and level metadata.

```python
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",
)
```

#### Properties

- `name: str`
- `grid_size: tuple[int, int] | None`

#### Methods

##### `__init__(sprites=None, grid_size=None, data={}, name="Level", placeable_areas=None)`
Initialize a new Level.

- `sprites`: Optional list of sprites to initialize the level with
- `grid_size`: Optional `(width, height)` tuple for grid sizing
- `data`: Optional metadata dictionary
- `name`: Level name

##### `add_sprite(sprite)`
Add a sprite to the level.

- `sprite`: The sprite to add

##### `remove_sprite(sprite)`
Remove a sprite from the level.

- `sprite`: The sprite to remove

##### `remove_all_sprites()`
Remove all sprites from the level.

##### `get_sprites()`
Get all sprites in the level.

- Returns: A copy of the sprite list

##### `get_sprites_by_name(name)`
Get all sprites with the given name.

- `name`: The name to search for
- Returns: List of sprites with the given name

##### `get_sprites_by_tag(tag)`
Get all sprites that have the given tag.

- `tag`: The tag to search for
- Returns: List of sprites that have the tag

##### `get_sprites_by_tags(tags)`
Get all sprites that have all of the given tags (AND).

- `tags`: Tags to search for
- Returns: List of sprites with all tags

##### `get_sprites_by_any_tag(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_tags()`
Get all unique tags from all sprites in the level.

- Returns: A set of tag strings

##### `get_sprite_at(x, y, tag=None, ignore_collidable=False)`
Get the top-most sprite at the given coordinates.

- `x`, `y`: Coordinates to search
- `tag`: Optional tag filter
- `ignore_collidable`: If True, includes non-collidable sprites
- Returns: The first matching sprite or `None`

##### `collides_with(sprite, ignoreMode=False)`
Return all sprites in the level that collide with the given sprite.

- `sprite`: The sprite to check for collisions
- `ignoreMode`: If True, bypasses interaction/blocking checks

##### `get_data(key)`
Get metadata by key.

- Returns: The stored value or `None`

##### `clone()`
Create a deep copy of this level.

- Returns: A new `Level` instance with cloned sprites

## Development

To set up the development environment:

1. Clone the repository:
   ```bash
   git clone git@github.com:arcprize/ARCEngine.git
   cd ARCEngine
   ```

2. Create and activate a virtual environment using uv:
   ```bash
   uv venv
   source .venv/bin/activate  # On Windows: .venv\Scripts\activate
   ```

3. Install development dependencies:
   ```bash
   uv sync
   ```

4. Install git hooks:
   ```bash
   pre-commit install
   ```

This repo uses `ruff` to lint/format and `mypy` for type checking:

```bash
pre-commit run --all-files
```

Note: 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/).

## Contributions

This project does not accept external contributions.

## Citation

If you use this project in your research, please cite it as:

```bibtex
@software{arc_agi,
  author       = {ARC Prize Foundation},
  title        = {ARC Game Engine},
  year         = {2026},
  url          = {https://github.com/arcprize/ARCEngine},
  version      = {0.9.3}
}
```

## License

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.
