Objects¶
Every cell on the grid can hold a GridObj instance. Objects define their appearance, behavior, and interaction rules through class attributes and methods.
GridObj Base Class¶
All grid objects inherit from GridObj:
from cogrid.core.objects.base import GridObj
class MyObject(GridObj):
object_id: str = None # set by @register_object_type
color: str | tuple # render color (name or RGB tuple)
char: str # single-character layout symbol
GridObj
GridObj
¶
Base class for all objects that can exist on a grid cell.
Initialize grid object with state.
Registration¶
Objects are registered with the @register_object_type decorator:
from cogrid.core.objects.registry import register_object_type
@register_object_type("my_object", scope="my_env")
class MyObject(GridObj):
color = (200, 100, 50)
char = "M"
Objects in a named scope are only visible to environments that use that scope. Global objects are available everywhere.
register_object_type
register_object_type
¶
register_object_type(object_id: str, scope: str = 'global') -> Callable[[type[GridObj]], type[GridObj]]
Register a GridObj subclass with static property metadata.
Stores boolean properties for build_lookup_tables() and
auto-discovers component classmethods (tick, interaction, etc.).
Capabilities are declared as class attributes using :func:when
descriptors or plain True::
@register_object_type("onion", scope="overcooked")
class Onion(GridObj):
can_pickup = when()
@register_object_type("wall")
class Wall(GridObj):
is_wall = True
Built-in Objects (Global Scope)¶
| Object | Char | Color | Behavior |
|---|---|---|---|
Wall |
# |
Grey | Impassable, blocks visibility |
Floor |
|
Pale Blue | Walkable empty tile |
Counter |
C |
Light Brown | Surface that holds one object on top |
Key |
K |
Yellow | Pickupable item |
Door |
D |
Dark Grey | Three states: locked (0), closed (1), open (2) |
Object Properties¶
Objects declare interaction rules through class-level descriptors:
from cogrid.core.objects.when import when
@register_object_type("counter")
class Counter(GridObj):
can_place_on = when() # agents can place held items on this
can_pickup_from = when() # agents can pick up items from this
The when() descriptor (with no arguments) means the action is always allowed. Pass agent_holding to restrict by held item type. The engine reads these descriptors at init time and compiles them into lookup tables.
when
Containers¶
Objects that hold multiple items use the Container descriptor:
from cogrid.core.objects.containers import Container
@register_object_type("pot", scope="overcooked")
class Pot(GridObj):
container = Container(capacity=3, pickup_requires="plate")
The autowire system reads container at init time and generates:
- Extra state arrays — contents, timer, and positions per container instance.
- Tick function — decrements timers when the container is full.
- Render sync — syncs container state back to
GridObjinstances for rendering. - Interaction branches — pickup/drop logic based on
capacityandpickup_requires.
Containers can be paired with environment-specific recipe systems. The Overcooked environment defines a Recipe dataclass that declares how ingredients combine into results inside a container.
Container
Container
dataclass
¶
Declares that a GridObj is a stateful container (e.g. a cooking pot).
Parameters¶
capacity : int
Maximum number of items the container can hold.
pickup_requires : str | list[str] | None
Object type(s) the agent must be holding to pick up from this
container. None means empty hands.
Tick Functions¶
A tick function runs once per step, before movement and interactions. It is the mechanism for time-dependent object behavior — cooking timers, object movement, state decay, anything that should happen every step regardless of agent actions.
Pattern¶
Define a build_tick_fn classmethod on your GridObj subclass. It returns a closure:
@register_object_type("pot", scope="overcooked")
class Pot(GridObj):
container = Container(capacity=3, pickup_requires="plate")
@classmethod
def build_tick_fn(cls):
def tick(state, scope_config):
# Read container arrays from extra_state
timer = state.extra_state[f"{scope_config['scope']}.pot_timer"]
contents = state.extra_state[f"{scope_config['scope']}.pot_contents"]
# Decrement timer for full containers (all slots nonzero)
full = (contents > 0).all(axis=-1)
cooking = full & (timer > 0)
new_timer = xp.where(cooking, timer - 1, timer)
new_extra = {**state.extra_state,
f"{scope_config['scope']}.pot_timer": new_timer}
return dataclasses.replace(state, extra_state=new_extra)
return tick
Signature¶
The returned closure must match:
state— the full environment state (grid arrays, agent positions, extra state).scope_config— dict with"type_ids","scope", and other init-time metadata.- Returns a new
EnvStatewith any modifications applied.
Discovery and composition¶
The autowire system discovers all build_tick_fn classmethods on registered objects and composes them sequentially. Container objects get a tick function automatically (see Containers above). There is nothing to wire manually — registering the object is enough.
The step pipeline runs all tick functions before movement and interactions:
tick → movement → interactions → observations → rewards
Other Lifecycle Hooks¶
Objects can define additional classmethods for initialization and rendering:
| Hook | Signature | Purpose |
|---|---|---|
extra_state_schema() |
cls -> dict |
Declare additional state arrays (shape, dtype). |
extra_state_builder() |
cls -> callable |
Return a factory that initializes extra state arrays at reset time. |
build_render_sync_fn() |
cls -> callable |
Return a callback that syncs array state back to GridObj instances for rendering. |
Example: Counter.build_render_sync_fn() returns a callback that reads object_state_map values and sets obj_placed_on on each Counter instance so items placed on counters are drawn correctly.