Add observation_function for full custom control over the observation
Lets a game define how its state becomes an observation as a plain Python function (module:attr), used identically by GameEnvironment (training) and TrainedPolicy (inference) instead of two independently maintained encoding paths. Removes the egocentric/egocentric_player/ egocentric_radius flags — cropping is now something an observation_function does itself by calling egocentric_board(), and extras_size is discovered from one sampled observation instead of being configured via observe_state_sizes.
This commit is contained in:
@@ -4,6 +4,7 @@ import tomllib
|
||||
import tomli_w
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -11,9 +12,15 @@ class GameMetadata:
|
||||
"""Describes a retro game for training purposes.
|
||||
|
||||
Required fields: actions, reward.
|
||||
Optional fields: character_set, spatial.
|
||||
Discovered fields: board_size (from game.board_size), extras_size (from
|
||||
the observe_state list in [preprocessing]).
|
||||
Optional fields: character_set, spatial, observation_function.
|
||||
Discovered fields: board_size (from game.board_size), extras_size
|
||||
(computed by DQNTrainer from one sampled observation — never set in a
|
||||
game's own pyproject.toml).
|
||||
|
||||
observation_function, if set, is a "module:attr" string naming a function
|
||||
``f(game) -> Any`` that fully replaces the built-in board/observe_state
|
||||
encoding. It is mutually exclusive with the [preprocessing] observe_state
|
||||
option. See GameEnvironment for how the two paths are selected.
|
||||
"""
|
||||
actions: list[str]
|
||||
reward: str
|
||||
@@ -21,6 +28,7 @@ class GameMetadata:
|
||||
spatial: bool = False
|
||||
board: bool = True
|
||||
board_size: tuple[int, int] | None = None
|
||||
observation_function: str | None = None
|
||||
extras_size: int = 0
|
||||
|
||||
def validate(self):
|
||||
@@ -59,6 +67,48 @@ class GameMetadata:
|
||||
"If you're not sure what characters your game uses, remove character_set\n"
|
||||
"entirely and the trainer will discover them automatically."
|
||||
)
|
||||
if self.observation_function is not None:
|
||||
if not isinstance(self.observation_function, str) or ':' not in self.observation_function:
|
||||
raise ValueError(
|
||||
f"'observation_function' must be a string of the form 'module:attr', "
|
||||
f"but got: {self.observation_function!r}\n"
|
||||
"Example: observation_function = \"my_game:get_observation\"\n"
|
||||
"This should name a function f(game) -> observation that fully\n"
|
||||
"describes what your agent observes each turn."
|
||||
)
|
||||
|
||||
def resolve_observation_function(self) -> Callable | None:
|
||||
"""Import and return the function named by observation_function, or None if unset.
|
||||
|
||||
Raises ValueError with an actionable message if the string isn't
|
||||
"module:attr", the module can't be imported, or it has no such attribute.
|
||||
"""
|
||||
if self.observation_function is None:
|
||||
return None
|
||||
if ':' not in self.observation_function:
|
||||
raise ValueError(
|
||||
f"'observation_function' must be of the form 'module:attr', but got "
|
||||
f"{self.observation_function!r} (no ':' found).\n"
|
||||
"Example: observation_function = \"my_game:get_observation\""
|
||||
)
|
||||
module_name, attr_name = self.observation_function.split(':', 1)
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as e:
|
||||
raise ValueError(
|
||||
f"Could not import module {module_name!r} for observation_function "
|
||||
f"{self.observation_function!r}: {e}\n"
|
||||
"Make sure the module is importable (e.g. on PYTHONPATH or installed)."
|
||||
) from e
|
||||
try:
|
||||
return getattr(module, attr_name)
|
||||
except AttributeError:
|
||||
raise ValueError(
|
||||
f"Module {module_name!r} has no attribute {attr_name!r} "
|
||||
f"(from observation_function = {self.observation_function!r}).\n"
|
||||
f"Define a function named '{attr_name}' in {module_name} that takes a "
|
||||
"game instance and returns its observation."
|
||||
) from None
|
||||
|
||||
@classmethod
|
||||
def from_pyproject(cls, module_name: str) -> GameMetadata:
|
||||
@@ -102,17 +152,22 @@ class GameMetadata:
|
||||
character_set=d.get('character_set'),
|
||||
spatial=d.get('spatial', False),
|
||||
board_size=board_size,
|
||||
observation_function=d.get('observation_function'),
|
||||
extras_size=d.get('extras_size', 0),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {
|
||||
'actions': self.actions,
|
||||
'reward': self.reward,
|
||||
'extras_size': self.extras_size,
|
||||
}
|
||||
if self.board_size is not None:
|
||||
d['board_size'] = list(self.board_size)
|
||||
if self.character_set is not None:
|
||||
d['character_set'] = self.character_set
|
||||
if self.observation_function is not None:
|
||||
d['observation_function'] = self.observation_function
|
||||
return d
|
||||
|
||||
def to_toml(self, path: str | Path):
|
||||
|
||||
Reference in New Issue
Block a user