Skip to content

Design ‐ New Automation System

Pier-Luc Brault edited this page May 16, 2026 · 3 revisions

WORK IN PROGRESS

This document will describe the architecture for upcoming updates that will improve on the current implementation of automation for the game.

Current Implementation

The current implementation of automation uses a dedicated entry point for the game in the file src/auto.py. This entry point is called with parameters indicating the difficulty level or sandbox config to use along with the filename of an automation script. The script is compiled using Python's built-in compile function, and passed to the Stage object that is created based on the provided difficulty level or sandbox config. The Stage object also has a standalone property that is set to True. The game is then launched on that stage directly, and the GameManager instance is instructed to ignore events (inputs).

The stage's setup method calls a private method named _prepare_automation_script. This method defines globals that the script will be able to access (num_cpus, num_ram_pages, num_swap_pages, cpu_core_types), then runs the compiled script using python's built-in exec function. At that point, the script sets a global named scheduler, that the stage retrieves and assigns to its _script_callback attribute. The scheduler global is an instance of a subclass of the Scheduler class defined in automation/api.py.

Each frame, the stage and stage's scene objects notify the game_monitor module of all important events happening in the game by calling functions such as notify_page_swap, notify_process_starvation, etc. The stage's _process_script_events private method is then called. That method executes self._script_callback(game_monitor.get_events()), which calls the scheduler's __call__ method defined in automation/api.py. The method receives all game events that were registered since last frame. It updates the scheduler's internal attributes, which are basically a mirror of the game's state. After that, the scheduler's schedule method, which contains the actual automation logic, is called. At the end, the scheduler returns a list of actions (ex: move a process to CPU), which are finally retrieved by the stage's _process_script_events and executed.

New Goals

Primary Goals

  • Implement a CLI that will allow an AI agent to play the game
  • Remove the need for the client to maintain its own representation of the game's state
  • Limit impacts on the automation code when implementing new features in the game (new game features should require minimal changes to the automation system)
  • Make the automation system suitable for E2E tests

Secondary Goals

  • Expose a direct API to control the game without using a script or a CLI.
    • The API implementation should be decoupled from the transport layer.
    • At least TCP and Websocket should be supported for transport, and it should be possible to implement other transport protocols in the future.
  • Make the API expose a text representation of the game interface and allow emulation of mouse and keyboard inputs as a more "low-level" mode of interaction with the game. This would be complementary to the "high-level" mode of the API exposing logical state and allowing to run logical actions.
  • Allow control of the whole game from the main menu, not just the stage.
  • Make automation also possible when running the game in a web browser.

Current Limitations

  • Stage code is tightly coupled to the automation system, which complicates maintenance as it increases the risk of breaking automation while making changes to the stage.
  • Automated actions are executed synchronously as part of the game loop, which would complicate the implementation of a CLI.
  • Automation script has to maintain its own representation of the game's state. This adds complexity and possibilities of bugs. It also makes automation-based E2E testing less reliable as it would test the automation client as much as the game itself.
  • Automated actions and player actions sometimes take different paths in the code, which also complicates the use of automation for E2E testing.
  • Only the stage can be interacted with, so complete E2E testing through the automation system is not supported.

Phase 1 - Terminology Change and New Class Hierarchy

This phase will implement small changes that will make it easier to implement the new automation API later. The current scripting system will continue to work as is after this phase.

The codebase currently uses both the terms actions and events to refer to mouse and keyboard inputs. This creates ambiguities as the words action and event are also used for in-game concepts (ex: I/O events, I/O queue action, process state events, etc). Furthermore, the word event is used to refer both to events sent to game_monitor for the purpose of notifying the automation script about them, and to events sent by the automation script to perform actions in the game.

The new architecture will clearly differentiate between these concepts:

  • Input will be the exclusive term used to refer to mouse and keyboard inputs. This involves changes to the engine code:
    • GameEvent will become Input
    • GameEventType will become InputType
    • Any use of the word event or events in the codebase to refer to player inputs will be changed to input or inputs
    • The update method of Scene will now have an inputs argument instead of events. Any subclass that currently renames this parameter to player_actions will change it to inputs instead.
  • Action will refer to a logical action that can be performed by the player (ex: assign a process to a CPU, swap a page, etc). In the final architecture, there will be no distinction between human player actions and automated actions.
  • Event will refer either to an in-game event not necessarily triggered by the player (ex: I/O event, process termination, etc), or to internal state machine events (see Process State Machine). The term event will need to always be prefixed by a context-appropriate term (ex: io_event, state_event).
  • Notification will replace Event to refer to information currently sent to game_monitor.

Engine Refactor

  • A new abstract class called GameObject will be introduced. Both Scene and SceneObject will inherit from that class.
    • Implemented by #222

  • Scene's _scene_objects and SceneObject's _children will be unified into GameObject's _children private attribute and children public property.

Game Monitor

  • The words event currently used in the game monitor will become notification, both for the module's public and private members.

Stage Scripting Methods

  • _process_script_events and _get_script_events will become _process_script_actions and _call_script_scheduler. Notifications will be sent to the script instead of events. Vocabulary used in the methods' code will be updated accordingly.

No change will be made to the vocabulary currently used in automation/api.py, the word "event" can stay in use script-side as this does not impact the game's codebase.

Phase 2 - New Action Architecture

This phase implements the new Action concept explicitly, and makes the changes that are necessary to solve the problem of player actions and automated actions following two different paths.

  • As defined in phase 1, action now refers to any logical action that can be performed by the player (ex: assign a process to a CPU, swap a page, etc), while input describes a raw mouse or keyboard input.
  • This phase will refactor the game's architecture to implement the concept of actions more explicitly. In the new architecture, an input with an effect on an object will generate an action instead of being processed directly.
  • The following members will be added to GameObject:
    • A run_action(type) abstract method
    • An action_types abstract class property that returns an enum of action types that are available for this object. The enum is specific to the subclass. Enum values should be strings that are identical to the enum names.
  • These changes are made to GameObject instead of SceneObjects, as a scene can also define actions (ex: "show the in-game menu").
  • All scenes and scene objects will have to implement the run_action method and the action_types property.
  • The update method of all scenes and scene objects will need to be changed to call run_action for every input that should trigger an action.
  • The _process_script_actions method of class Stage needs to be updated to call run_action instead of processing actions directly.

Phase 3 - Public State and View Refactor

This phase will implement a unified interface to access game objects' public state. The same interface will be used both by the game's UI and the future API.

  • The following attributes will be added to GameObject:
    • An abstract state_dict property that returns a dictionary describing the current public state of the object
    • An abstract attr_dict property that returns a dictionary containing public attributes of the object
      • The difference between state_dict and attr_dict is that state_dict changes throughout the object's life-cycle while attr_dict is static (ex: a process' PID is part of its attr_dict, not its state_dict). attr_dict provides "logical" identification for the object other than the object_id.
  • SceneObject already has a view_vars property that exposes a dictionary containing extra information needed by the object's view. The current implementation for this property will remain as is.
  • The Drawable class will be renamed to View as it has no other use than defining a scene object's view.
  • The following changes will be made to views:
    • All views will be refactored to not hold a reference to the corresponding scene object anymore. They will instead receive the state_dict, attr_dict view_vars dictionaries. In the future, this change will organically ensure that all information that is exposed to a human player will also be exposed to the API.
    • The constructor will receive an attr_dict parameter
    • The draw method will receive state_dict and view_vars
  • All scene objects will need to be updated to accommodate the changes described above.
    • In some cases, this will involve more important changes that will require more thorough design on a per-object basis. For example, ProcessView currently accesses self._process.cpu.process_happiness_ms, which will need to be addressed in the new architecture.
  • In the case of Scene, its state_dict and attr_dict dictionaries will probably be empty for now, but will be useful in the future to communicate scene metadata to the API (ex: current difficulty level).

Phase 4 - Serialization of Game State

  • GameObject needs a new object_id property that returns a unique identifier for the object. The value returned could probably be that of id(self), but the actual implementation of object_id is irrelevant and the API should make no guarantee on the format or even the return type of object_id.
  • Add an abstract serialize method to GameObject. This method returns a dictionary.
  • Scene's implementation of serialize should return a dictionary in the following format:
{
  "object_id": <object_id>,
  "object_type": <classname>,
  "attrs": <attrs_dict>,
  "state": <state_dict>,
  "available_actions": [<action_type_string>, ...]
  "children": [<serialized_scene_object>, ...],
}
  • SceneObject's implementation of serialize should return a dictionary in the same format, but with a view_vars key added.
  • GameManager needs to expose a get_serialized_game_state method. This method should return a dictionary in this format:
{
  "current_scene": <serialized_scene>
}

Phase X

This is currently unstructured and incomplete. Architecture changes that are not needed for previous phases were moved here.

  • GameObject will also have:
    • A propagate_action(target, type) method. target is an object ID and type is a value from action_types. The method either calls run_action if the target is self, or propagate_action to each object in children if the target is another object. If the target is not self and the object has no child, the action is dropped. This allows to recursively propagate the action in the object tree until it reaches its target.