Spec: ActionSpace¶
Formal contract for all action space implementations.
Identity¶
| Property | Value |
|---|---|
| Abstract base | ActionSpace |
| File | src/ppage/actions/base.py |
| Registry | action_registry.py |
| Extensible by | All contributors |
Required Interface¶
class MyActionSpace(ActionSpace):
def __init__(self, **params):
super().__init__(**params)
def to_direction(self, action: int) -> np.ndarray:
# return [dx, dy] for the given action integer
...
@property
def gymnasium_space(self) -> spaces.Space:
# return the gymnasium space matching this action set
...
@property
def n_actions(self) -> int:
# number of valid action integers (0 to n_actions-1)
...
to_direction(action) Contract¶
Preconditions¶
actionis an integer in the range[0, n_actions - 1]
Postconditions¶
- Returns an
np.ndarrayof shape(2,)and dtype compatible withint32 - The vector represents
[dx, dy]in grid coordinates (origin top-left, Y increases downward) - For the same
actioninput, always returns the same vector (deterministic) - Raises
ValueErrorfor out-of-range action integers (do not silently return zero)
What to_direction() must NOT do¶
- Modify
envstate - Access
envat all — direction lookup must be a pure function of the action integer - Return vectors with magnitude > 1 (single-step movement only)
gymnasium_space Contract¶
- Must return a
gymnasium.spaces.Space - Must be consistent with
n_actions:gymnasium_space.n == n_actionsforDiscretespaces - Must be stable — same object or equivalent across calls
n_actions Contract¶
- Returns the count of valid action integers
- Valid integers are
0, 1, ..., n_actions - 1
What to_direction() May Access¶
| Resource | Allowed? |
|---|---|
self.params (read) |
✅ |
| Instance attributes (read) | ✅ |
env or agent state |
❌ |
| External randomness | ❌ |
Registration¶
Every action space must be registered before it can be used via config:
# In action_registry.py — add to _ACTION_REGISTRY:
"my_action_space": MyActionSpace,
# Or at runtime:
from ppage.registry.action_registry import register_action_space
register_action_space("my_action_space", MyActionSpace)
Like
register_reward()andregister_observation(),register_action_space()validatesissubclass(cls, ActionSpace)— registering an incompatible class raisesTypeError("Action space must inherit from ActionSpace")immediately.
The registry key must match what is in actions.yaml:
Wiring¶
The action space is set on the environment before training begins:
step() calls env.action_space_plugin.to_direction(a) when the plugin is set, and falls back to ag._actions_to_directions[a] otherwise.
SpeedWrapperusesenv.action_space_plugin. It reads NOOP-ness viaplugin.is_noop(action)and derives the idle-slot action index from the same configured plugin, so a custom action space with a different NOOP index works correctly. (to_moves()onSpeedDiscreteActionSpacestill exists but the wrapper no longer uses it.) See Wrappers.
Checklist for New Action Spaces¶
Before submitting a new action space:
- [ ] Inherits from
ActionSpace - [ ]
to_direction()raisesValueErrorfor invalid action integers - [ ]
gymnasium_space.n == n_actions - [ ] Direction vectors are
np.ndarrayof shape(2,), dtype compatible withint32 - [ ] All direction vectors have component values in
{-1, 0, +1}(single-step) - [ ]
__init__uses only**kwargs(no positional args beyondself) - [ ] Registered in
action_registry.pywith correct key - [ ] Exported from
actions/__init__.py - [ ] Added an entry to concepts/actions.md