Source code for mini_arcade_core.engine.gameplay_settings

"""
Gameplay settings that can be modified during gameplay.
"""

from __future__ import annotations

from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any, Literal, cast

from mini_arcade_core.backend.keys import Key
from mini_arcade_core.engine.render.effects.base import EffectStack

DifficultyType = Literal["easy", "normal", "hard", "insane"]
_VALID_DIFFICULTIES = ("easy", "normal", "hard", "insane")
_DEFAULT_DEBUG_SECTIONS = (
    "timing",
    "render",
    "viewport",
    "effects",
    "stack",
    "scene",
)


def _normalize_difficulty(value: Any) -> DifficultyType:
    normalized = str(value).strip().lower()
    if normalized in _VALID_DIFFICULTIES:
        return cast(DifficultyType, normalized)
    return "normal"


def _normalize_key(value: Any) -> Key | None:
    if isinstance(value, Key):
        return value
    if value is None or value is False:
        return None

    normalized = str(value).strip().upper()
    if not normalized:
        return None
    try:
        return Key[normalized]
    except KeyError:
        return None


def _normalize_color(value: Any, default: tuple[int, ...]) -> tuple[int, ...]:
    if not isinstance(value, (list, tuple)) or not value:
        return default
    out: list[int] = []
    for component in value:
        try:
            out.append(int(component))
        except (TypeError, ValueError):
            return default
    return tuple(out)


[docs] @dataclass class DifficultySettings: """ Settings related to game difficulty that can be modified during gameplay. :ivar level (DifficultyType): Current difficulty level. """ level: DifficultyType = "normal"
[docs] @classmethod def from_dict(cls, data: dict[str, Any] | None) -> "DifficultySettings": """ Construct difficulty settings from a dict, typically parsed from a game config file. :param data: The input data to parse. :type data: dict or None :return: A DifficultySettings instance populated with the parsed data. :rtype: DifficultySettings """ if not isinstance(data, dict): return cls() raw_level = data.get("level", data.get("default", "normal")) return cls(level=_normalize_difficulty(raw_level))
# Justification: Next 2 classes are a bit long but they're mostly data parsing and construction # of the settings from a dict, hard to break down more without overcomplicating it. # pylint: disable=too-many-instance-attributes
[docs] @dataclass class DebugOverlayStyleSettings: """ Visual configuration for the built-in debug overlay. """ x: int = 8 y: int = 8 width: int = 360 padding: int = 8 line_height: int = 18 font_size: int = 14 panel_color: tuple[int, ...] = (0, 0, 0, 166) text_color: tuple[int, ...] = (255, 255, 255, 255)
[docs] @classmethod def from_dict( cls, data: dict[str, Any] | None ) -> "DebugOverlayStyleSettings": """ Construct debug overlay style settings from a dict, typically parsed from a game config file. :param data: The input data to parse. :type data: dict or None :return: A DebugOverlayStyleSettings instance populated with the parsed data. :rtype: DebugOverlayStyleSettings """ if not isinstance(data, dict): return cls() defaults = cls() return cls( x=int(data.get("x", defaults.x)), y=int(data.get("y", defaults.y)), width=int(data.get("width", defaults.width)), padding=int(data.get("padding", defaults.padding)), line_height=int(data.get("line_height", defaults.line_height)), font_size=int(data.get("font_size", defaults.font_size)), panel_color=_normalize_color( data.get("panel_color"), defaults.panel_color ), text_color=_normalize_color( data.get("text_color"), defaults.text_color ), )
[docs] @dataclass class DebugOverlaySettings: """ Gameplay-configurable debug overlay settings. """ enabled: bool = False start_visible: bool = False scene_id: str = "debug_overlay" toggle_key: Key | None = Key.F1 title: str = "Debug Overlay" sections: tuple[str, ...] = _DEFAULT_DEBUG_SECTIONS static_lines: list[str] = field(default_factory=list) style: DebugOverlayStyleSettings = field( default_factory=DebugOverlayStyleSettings )
[docs] @classmethod def from_dict(cls, data: Any) -> "DebugOverlaySettings": """ Construct debug overlay settings from a dict, typically parsed from a game config file. :param data: The input data to parse. :type data: dict or any :return: A DebugOverlaySettings instance populated with the parsed data. :rtype: DebugOverlaySettings """ defaults = cls() if isinstance(data, bool): return cls(enabled=bool(data)) if not isinstance(data, dict): return defaults raw_sections = data.get("sections", list(defaults.sections)) if isinstance(raw_sections, (list, tuple)): sections = tuple( str(item).strip().lower() for item in raw_sections if str(item).strip() ) else: sections = defaults.sections raw_static_lines = data.get("static_lines", []) static_lines = ( [str(item) for item in raw_static_lines if str(item).strip()] if isinstance(raw_static_lines, list) else [] ) return cls( enabled=bool(data.get("enabled", defaults.enabled)), start_visible=bool( data.get("start_visible", defaults.start_visible) ), scene_id=str(data.get("scene_id", defaults.scene_id)).strip() or defaults.scene_id, toggle_key=_normalize_key( data.get("toggle_key", defaults.toggle_key) ), title=str(data.get("title", defaults.title)), sections=sections or defaults.sections, static_lines=static_lines, style=DebugOverlayStyleSettings.from_dict(data.get("style")), )
# pylint: enable=too-many-instance-attributes
[docs] @dataclass class SceneActionSettings: """ Declarative command configuration for scene-level actions. """ command: str scene_id: str | None = None as_overlay: bool = False
[docs] @classmethod def from_dict(cls, data: Any) -> "SceneActionSettings | None": """ Construct scene action settings from a dict, typically parsed from a game config file. :param data: The input data to parse. :type data: dict or any :return: A SceneActionSettings instance populated with the parsed data, or None if the input data is invalid or does not specify a command. :rtype: SceneActionSettings or None """ if isinstance(data, str): command = str(data).strip().lower() return cls(command=command) if command else None if not isinstance(data, dict): return None command = str(data.get("command", "")).strip().lower() if not command: return None target_scene = data.get("scene_id", data.get("target_scene")) scene_id = ( str(target_scene).strip() if target_scene is not None else None ) if scene_id == "": scene_id = None return cls( command=command, scene_id=scene_id, as_overlay=bool(data.get("as_overlay", False)), )
[docs] @dataclass class SceneRuntimeSettings: """ Per-scene gameplay behavior configuration. """ escape: SceneActionSettings | None = None data: dict[str, Any] = field(default_factory=dict)
[docs] @classmethod def from_dict(cls, data: Any) -> "SceneRuntimeSettings": """ Construct scene runtime settings from a dict, typically parsed from a game config file. :param data: The input data to parse. :type data: dict or any :return: A SceneRuntimeSettings instance populated with the parsed data. :rtype: SceneRuntimeSettings """ if not isinstance(data, dict): return cls() payload = deepcopy(data) payload.pop("escape", None) return cls( escape=SceneActionSettings.from_dict(data.get("escape")), data=payload, )
[docs] def get(self, key: str, default: Any = None) -> Any: """ Get arbitrary data from the scene settings, for use by game code. :param key: The key to look up in the scene settings data. :type key: str :param default: The default value to return if the key is not found. :type default: Any :return: The value associated with the key in the scene settings data, or the default if not found. :rtype: Any """ return deepcopy(self.data.get(key, default))
[docs] @dataclass class GamePlaySettings: """ Game settings that can be modified during gameplay. :ivar difficulty (DifficultySettings): Current game difficulty settings. """ difficulty: DifficultySettings = field(default_factory=DifficultySettings) controls: dict[str, Any] = field(default_factory=dict) effects_stack: EffectStack | None = None debug_overlay: DebugOverlaySettings = field( default_factory=DebugOverlaySettings ) scenes: dict[str, SceneRuntimeSettings] = field(default_factory=dict)
[docs] @classmethod def from_dict(cls, data: dict[str, Any] | None) -> "GamePlaySettings": """ Construct gameplay settings from a dict, typically parsed from a game config file. :param data: The input data to parse. :type data: dict or None :return: A GamePlaySettings instance populated with the parsed data. :rtype: GamePlaySettings """ settings = cls() if not isinstance(data, dict): return settings raw_difficulty = data.get("difficulty") if isinstance(raw_difficulty, str): settings.difficulty = DifficultySettings( level=_normalize_difficulty(raw_difficulty) ) elif isinstance(raw_difficulty, dict): settings.difficulty = DifficultySettings.from_dict(raw_difficulty) raw_controls = data.get("controls") if isinstance(raw_controls, dict): settings.controls = deepcopy(raw_controls) settings.debug_overlay = DebugOverlaySettings.from_dict( data.get("debug_overlay") ) raw_scenes = data.get("scenes") if isinstance(raw_scenes, dict): settings.scenes = { str(scene_id): SceneRuntimeSettings.from_dict(scene_data) for scene_id, scene_data in raw_scenes.items() if str(scene_id).strip() } return settings
[docs] def scene_settings(self, scene_id: str) -> SceneRuntimeSettings | None: """ Resolve runtime scene settings by registered scene id. """ return self.scenes.get(str(scene_id))