Build your own ARC-AGI-3 game with arcengine in 13 lines

A working ARC-AGI-3 game in 13 lines of Python on arcengine: the class, the camera, two levels, step(), the on-disk layout the toolkit scans, and a local score.

0:00
Build your own ARC-AGI-3 game with arcengine in 13 lines

A working ARC-AGI-3 game is 13 lines of Python and one line of JSON. Written, dropped into the toolkit’s environment directory, and loaded in offline mode on a studio test machine, it rendered a 64 by 64 frame, advanced through two levels on two key presses, declared a win, and produced a scorecard, without touching the network.

arcengine is the engine under every one of the 25 public games, and it turns out to ask very little of a game author. Here is the smallest thing that satisfies it, and what each part is for.

What arcengine needs from a game: a class, a camera, levels and step()

Every downloaded game is a subclass of ARCBaseGame, and reading that base class shows the contract. The subclass constructs its levels and a camera, passes them up with a game id and a list of available action ids, and overrides step(). That is the whole interface. The engine owns the action loop, the resets, the rendering and the score.

The smallest game that exercises all of it:

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

dot = Sprite(pixels=[[9]], name="dot", x=3, y=3)
levels = [
    Level(sprites=[dot.clone()], name="one"),
    Level(sprites=[dot.clone().set_position(8, 8)], name="two"),
]

class Zz01(ARCBaseGame):
    def __init__(self):
        super().__init__(game_id="zz01", levels=levels,
                         camera=Camera(width=16, height=16, background=0, letter_box=5),
                         available_actions=[1, 2])

    def step(self):
        if self.action.id == GameAction.ACTION1:
            self.next_level()
        elif self.action.id == GameAction.ACTION2:
            self.lose()
        self.complete_action()

A Sprite is a pixel array plus a position; [[9]] is a single cell of colour nine. A Level is a list of sprites, cloned from a palette so that resets can rebuild them from clean copies. The Camera is 16 by 16 here, the same size ls20 uses, and the engine scales it to the fixed 64 by 64 output. available_actions is the list of ids the frame will advertise to an agent, and nothing stops you declaring ids your step() ignores.

step() is the game. The engine calls it after storing the incoming action in self.action, and keeps calling it until the game calls complete_action(). Here every action completes in one step: key one advances a level, key two loses, anything else does nothing.

How to lay out an arcengine game on disk so the toolkit finds it

The toolkit does not import games from your code. It scans a directory for them, the same way it treats the ones it downloads, so a custom game has to look like a downloaded one:

environment_files/
  zz01/
    0000/
      zz01.py
      metadata.json

The version directory can be any string; the public games use an eight character hash. metadata.json needs a game id in the id-version form, which the docs give as the only required field, plus the human baselines the scorer will divide by:

{"game_id": "zz01-0000", "title": "ZZ01", "tags": ["keyboard"], "baseline_actions": [1, 1]}

With that in place, offline mode picks it up on construction. On this machine Arcade() listed zz01-0000 alongside the 25 real games, and make("zz01") loaded the class from the file and returned a frame. No registration, no key, and no difference in how the wrapper treats it. The class name is not a convention but a rule. The toolkit’s page on creating an environment states it: “The class name must match the 4-character game ID with the first letter capitalized”, so Zz01 for zz01, and it confirms that “The Toolkit derives the local directory from the location of metadata.json“. The same page lists __init__(self, seed: int = 0), on_set_level and step as the methods to implement; the toy above skips the seed argument and on_set_level and still loads, because the base class supplies defaults for both.

How to make a level end, and a game end, in arcengine

Three calls on the base class cover every outcome, and all three are marked final so a game cannot redefine them. next_level() adds one to the score, which the toolkit reports as levels_completed, and either queues the next level or, on the last one, calls win(). lose() sets the state to GAME_OVER. win() sets it to WIN.

The test run shows them in sequence. After RESET the frame reported levels_completed 0, state NOT_FINISHED, available actions [1, 2]. After one ACTION1: levels 1, still NOT_FINISHED, and the dot now drawn at the second level’s position. After another: levels 2, state WIN. A further ACTION1 after that returned a frame with an empty grid and the state still WIN, which is the engine refusing to run a finished game rather than an error, and it is the same behaviour that catches agents after a game over.

What a level looks like when it is complete is entirely up to step(). The engine has no notion of a goal cell or a score threshold. A game is whatever sequence of checks you write before calling next_level(), which is why the 25 public games can be so different from each other on the same engine, and why their step() methods are the part that ships obfuscated.

What the frame looks like for a one pixel arcengine sprite

The first frame back from zz01 was a 64 by 64 array containing exactly two colours: 0, the background passed to the camera, and 9, the dot. The camera is 16 wide, so the single cell at position (3, 3) becomes a 4 by 4 block of nines in the output, with the rest background. The letterbox colour, 5, did not appear at all, because a square 16 by 16 view scales to fill the square output with nothing left over.

That scaling is the reason a real game’s frame reads in runs of four identical values, and it is worth building a toy like this just to see it once. A frame is not the level; it is the camera’s view of the level, scaled up, with any interface elements drawn on top. The technical report fixes what an agent sees at a 64 by 64 grid of 16 colours, and the camera is how a level of any size meets that contract.

How to score your own arcengine game

Because the baselines live in metadata.json, the local scorecard works on a home made game exactly as it does on a downloaded one. The two levels above have a baseline of one action each, and the test run used one action for each, so the scorer gives both levels 100. Set the baselines to 10 and the same two presses score 115 each, the capped maximum, because the metric rewards beating the human by a margin and then stops.

That makes a custom game a clean test bed for a harness. You know the rules, you know the optimal action count, and you can set the human baseline to anything, so a new agent can be checked against a game with no unknowns before it is pointed at one with nothing but unknowns. The engine that runs the benchmark is the same engine that runs the toy, and it took 13 lines to prove it.

Share this