irregular6612's picture
refactor: restructure proteus into game/web subpackages
426093b
Raw
History Blame Contribute Delete
5.72 kB
"""
Module containing enums used throughout the ARCEngine.
"""
from __future__ import annotations
import json
from enum import Enum, auto
from typing import Any, List, Optional, Type, Union
from numpy import ndarray
from pydantic import BaseModel, Field, PrivateAttr, field_validator
MAX_REASONING_BYTES = 16 * 1024 # 16 KB guard-rail
class BlockingMode(Enum):
"""Enum defining different collision detection behaviors for sprites."""
NOT_BLOCKED = auto() # No collision detection
BOUNDING_BOX = auto() # Simple rectangular collision detection
PIXEL_PERFECT = auto() # Precise pixel-level collision detection
class InteractionMode(Enum):
"""Enum defining how a sprite interacts with the game world visually and physically."""
TANGIBLE = auto() # Visible and can be collided with
INTANGIBLE = auto() # Visible but cannot be collided with (ghost-like)
INVISIBLE = auto() # Not visible but can be collided with (invisible wall)
REMOVED = auto() # Not visible and cannot be collided with (effectively removed)
class GameState(str, Enum):
NOT_PLAYED = "NOT_PLAYED"
NOT_FINISHED = "NOT_FINISHED"
WIN = "WIN"
GAME_OVER = "GAME_OVER"
class SimpleAction(BaseModel):
game_id: str = ""
class ComplexAction(BaseModel):
game_id: str = ""
x: int = Field(0, ge=0, le=63)
y: int = Field(0, ge=0, le=63)
class GameAction(Enum):
RESET = (0, SimpleAction)
ACTION1 = (1, SimpleAction)
ACTION2 = (2, SimpleAction)
ACTION3 = (3, SimpleAction)
ACTION4 = (4, SimpleAction)
ACTION5 = (5, SimpleAction)
ACTION6 = (6, ComplexAction)
ACTION7 = (7, SimpleAction)
action_type: Union[Type[SimpleAction], Type[ComplexAction]]
action_data: Union[SimpleAction | ComplexAction]
def __init__(
self,
action_id: int,
action_type: Union[Type[SimpleAction], Type[ComplexAction]],
) -> None:
self._value_ = action_id
self.action_type = action_type
self.action_data = action_type()
def is_simple(self) -> bool:
return self.action_type is SimpleAction
def is_complex(self) -> bool:
return self.action_type is ComplexAction
def validate_data(self, data: dict[str, Any]) -> bool:
"""Raise exception on invalid parse of incoming JSON data."""
self.action_type.model_validate(data)
return True
def set_data(self, data: dict[str, Any]) -> Union[SimpleAction | ComplexAction]:
self.action_data = self.action_type(**data)
return self.action_data
@classmethod
def from_id(cls, action_id: int) -> GameAction:
for action in cls:
if action.value == action_id:
return action
raise ValueError(f"No GameAction with id {action_id}")
@classmethod
def from_name(cls, name: str) -> "GameAction":
try:
return cls[name.upper()]
except KeyError:
raise ValueError(f"No GameAction with name '{name}'")
@classmethod
def all_simple(cls) -> list[GameAction]:
return [a for a in cls if a.is_simple()]
@classmethod
def all_complex(cls) -> list[GameAction]:
return [a for a in cls if a.is_complex()]
class ActionInput(BaseModel):
id: GameAction = GameAction.RESET
data: dict[str, Any] = {}
reasoning: Optional[Any] = Field(default=None, description="Opaque client-supplied blob; stored & echoed back verbatim.")
# Optional size / serialisability guard
@field_validator("reasoning")
@classmethod
def _check_reasoning(cls, v: Any) -> Any:
if v is None:
return v # field omitted → fine
try:
raw = json.dumps(v, separators=(",", ":")).encode("utf-8")
except (TypeError, ValueError):
raise ValueError("reasoning must be JSON-serialisable")
if len(raw) > MAX_REASONING_BYTES:
raise ValueError(f"reasoning exceeds {MAX_REASONING_BYTES} bytes")
return v
class FrameData(BaseModel):
game_id: str = ""
frame: list[list[list[int]]] = []
state: GameState = GameState.NOT_PLAYED
levels_completed: int = Field(0, ge=0, le=254)
win_levels: int = Field(0, ge=0, le=254)
action_input: ActionInput = Field(default_factory=lambda: ActionInput())
guid: Optional[str] = None
full_reset: bool = False
available_actions: list[int] = []
def is_empty(self) -> bool:
return len(self.frame) == 0
def __str__(self) -> str:
return self.model_dump_json(indent=2)
class FrameDataRaw(BaseModel):
game_id: str = ""
state: GameState = GameState.NOT_PLAYED
levels_completed: int = 0
win_levels: int = 0
action_input: ActionInput = Field(default_factory=ActionInput)
guid: Optional[str] = None
full_reset: bool = False
available_actions: list[int] = Field(default_factory=list)
# runtime-only, not validated, not serialized
_frame: List[ndarray] = PrivateAttr(default_factory=list)
@property
def frame(self) -> List[ndarray]:
return self._frame
@frame.setter
def frame(self, value: List[ndarray]) -> None:
self._frame = value
def is_empty(self) -> bool:
return len(self._frame) == 0
def __str__(self) -> str:
return self.model_dump_json(indent=2)
class PlaceableArea:
x: int = 0
y: int = 0
width: int = 0
height: int = 0
x_scale: int = 1
y_scale: int = 1
def __init__(self, x: int, y: int, width: int, height: int, x_scale: int = 1, y_scale: int = 1):
self.x = x
self.y = y
self.width = width
self.height = height
self.x_scale = x_scale
self.y_scale = y_scale