Source code for mini_arcade_core.engine.cheats

"""
Cheats module for Mini Arcade Core.
Provides cheat codes and related functionality.
"""

from __future__ import annotations

from collections import deque
from dataclasses import dataclass, field
from typing import Callable, Deque, Dict, Optional, Sequence, TypeVar

from mini_arcade_core.engine.commands import Command, CommandQueue
from mini_arcade_core.runtime.input_frame import InputFrame

# Justification: We want to keep the type variable name simple here.
# pylint: disable=invalid-name
TContext = TypeVar("TContext")
# pylint: enable=invalid-name


[docs] @dataclass(frozen=True) class CheatCode: """ Represents a registered cheat code. :ivar name (str): Unique name of the cheat code. :ivar sequence (tuple[str, ...]): Sequence of key strings that trigger the cheat. :ivar action (CheatAction): BaseCheatCommand to call when the cheat is activated. :ivar clear_buffer_on_match (bool): Whether to clear the input buffer after a match. :ivar enabled (bool): Whether the cheat code is enabled. """ name: str sequence: tuple[str, ...] command_factory: Optional[Callable[[TContext], Command]] = None clear_buffer_on_match: bool = False enabled: bool = True
[docs] @dataclass class CheatManager: """ Reusable cheat code matcher. Keeps a rolling buffer of recent keys and triggers callbacks on sequence match. """ buffer_size: int = 16 enabled: bool = True _buffer: Deque[str] = field(default_factory=lambda: deque(maxlen=16)) _codes: Dict[str, CheatCode[TContext]] = field(default_factory=dict)
[docs] def __post_init__(self): # ensure deque maxlen matches buffer_size self._buffer = deque(maxlen=self.buffer_size)
@property def buffer(self) -> Sequence[str]: """ Get a snapshot of the current input buffer. :return: Current input buffer as a sequence of key strings. :rtype: Sequence[str] """ return tuple(self._buffer) # TODO: ISolve too-many-arguments warning later # Justification: The method needs multiple optional parameters for flexibility. # pylint: disable=too-many-arguments
[docs] def register( self, name: str, *, sequence: Sequence[str], command_factory: Callable[[TContext], Command], clear_buffer_on_match: bool = False, enabled: bool = True, ): """ Register a new cheat code. :param name: Unique name of the cheat code. :type name: str :param sequence: Sequence of key strings that trigger the cheat. :type sequence: Sequence[str] :param command_factory: Factory function to create the Command when the cheat is activated. :type command_factory: Callable[[TContext], Command] :param clear_buffer_on_match: Whether to clear the input buffer after a match. :type clear_buffer_on_match: bool :param enabled: Whether the cheat code is enabled. :type enabled: bool :raises ValueError: If name is empty or sequence is empty. """ if not name: raise ValueError("Cheat name must be non-empty.") if not sequence: raise ValueError(f"Cheat '{name}' sequence must be non-empty.") norm_seq = tuple(self._norm(s) for s in sequence) self._codes[name] = CheatCode( name=name, sequence=norm_seq, command_factory=command_factory, clear_buffer_on_match=clear_buffer_on_match, enabled=enabled, )
# pylint: enable=too-many-arguments
[docs] def process_frame( self, input_frame: InputFrame, *, context: TContext, queue: CommandQueue, ) -> list[str]: """ Process an InputFrame for cheat code matches. :param input_frame: InputFrame containing current inputs. :type input_frame: InputFrame :param context: Context to pass to command factories. :type context: TContext :param queue: CommandQueue to push matched commands into. :type queue: CommandQueue :return: List of names of matched cheat codes. :rtype: list[str] """ if not self.enabled: return [] matched: list[str] = [] for key in input_frame.keys_pressed: key_name = getattr(key, "name", str(key)) matched.extend( self.process_key(key_name, context=context, queue=queue) ) return matched
[docs] def process_key( self, key: str, *, context: TContext, queue: CommandQueue ) -> list[str]: """ Process a single key input. :param key: The key string to process. :type key: str :param context: Context to pass to command factories. :type context: TContext :param queue: CommandQueue to push matched commands into. :type queue: CommandQueue :return: List of names of matched cheat codes. :rtype: list[str] """ if not self.enabled: return [] k = self._norm(key) if not k: return [] self._buffer.append(k) buf = tuple(self._buffer) matched: list[str] = [] for code in self._codes.values(): if not code.enabled: continue seq = code.sequence if len(seq) > len(buf): continue if buf[-len(seq) :] == seq: queue.push(code.command_factory(context)) matched.append(code.name) if code.clear_buffer_on_match: self._buffer.clear() break return matched
@staticmethod def _norm(s: str) -> str: return s.strip().upper()