"""Effect handler registry for Mantimon TCG. This module implements the registration and lookup system for effect handlers. Effect handlers are functions that implement card effects like "deal 30 damage" or "flip a coin, if heads draw 2 cards". The registry uses a decorator pattern for registration: @effect_handler("deal_damage") def handle_deal_damage(ctx: EffectContext) -> EffectResult: amount = ctx.get_int_param("amount") target = ctx.get_target_pokemon() target.damage += amount return EffectResult.success_result(f"Dealt {amount} damage") Effects are then resolved by ID at runtime: result = resolve_effect("deal_damage", context) This decouples card definitions (which just store effect_id and params) from the actual effect implementations, allowing cards to be defined in JSON/database while effect logic lives in Python. """ from collections.abc import Callable from app.core.effects.base import EffectContext, EffectResult # Type alias for effect handler functions EffectHandler = Callable[[EffectContext], EffectResult] # Global registry mapping effect_id to handler function _EFFECT_REGISTRY: dict[str, EffectHandler] = {} def effect_handler(effect_id: str) -> Callable[[EffectHandler], EffectHandler]: """Decorator to register an effect handler. Use this decorator to register a function as the handler for an effect_id. The effect_id should match what's stored in CardDefinition.effect_id or Attack.effect_id. Args: effect_id: Unique identifier for this effect (e.g., "deal_damage"). Returns: Decorator function that registers the handler. Raises: ValueError: If effect_id is already registered. Example: @effect_handler("deal_damage") def handle_deal_damage(ctx: EffectContext) -> EffectResult: amount = ctx.get_int_param("amount") target = ctx.get_target_pokemon() if target is None: return EffectResult.failure("No target") target.damage += amount return EffectResult.success_result(f"Dealt {amount} damage") """ def decorator(func: EffectHandler) -> EffectHandler: if effect_id in _EFFECT_REGISTRY: raise ValueError(f"Effect handler already registered for '{effect_id}'") _EFFECT_REGISTRY[effect_id] = func return func return decorator def resolve_effect(effect_id: str, ctx: EffectContext) -> EffectResult: """Look up and execute an effect handler. Args: effect_id: The effect identifier to resolve. ctx: The EffectContext containing game state and parameters. Returns: The EffectResult from the handler, or a failure result if not found. Example: ctx = EffectContext(game=game, source_player_id="player1", rng=rng, params={"amount": 30}) result = resolve_effect("deal_damage", ctx) if result.success: print(result.message) """ handler = _EFFECT_REGISTRY.get(effect_id) if handler is None: return EffectResult.failure(f"Unknown effect: {effect_id}") try: return handler(ctx) except Exception as e: # Catch any exceptions from handlers to prevent game crashes return EffectResult.failure(f"Effect '{effect_id}' failed: {e}") def get_handler(effect_id: str) -> EffectHandler | None: """Get a handler function without executing it. Useful for inspection or testing. Args: effect_id: The effect identifier to look up. Returns: The handler function, or None if not registered. """ return _EFFECT_REGISTRY.get(effect_id) def is_registered(effect_id: str) -> bool: """Check if an effect is registered. Args: effect_id: The effect identifier to check. Returns: True if a handler is registered for this effect_id. """ return effect_id in _EFFECT_REGISTRY def list_effects() -> list[str]: """List all registered effect IDs. Returns: Sorted list of all registered effect identifiers. """ return sorted(_EFFECT_REGISTRY.keys()) def clear_registry() -> None: """Clear all registered handlers. WARNING: This is primarily for testing. Do not call in production code as it will break effect resolution. """ _EFFECT_REGISTRY.clear() def register_handler(effect_id: str, handler: EffectHandler) -> None: """Programmatically register an effect handler. This is an alternative to using the @effect_handler decorator. Useful for dynamic registration or testing. Args: effect_id: Unique identifier for this effect. handler: The handler function to register. Raises: ValueError: If effect_id is already registered. Example: def my_handler(ctx: EffectContext) -> EffectResult: return EffectResult.success_result("Custom effect") register_handler("custom_effect", my_handler) """ if effect_id in _EFFECT_REGISTRY: raise ValueError(f"Effect handler already registered for '{effect_id}'") _EFFECT_REGISTRY[effect_id] = handler def unregister_handler(effect_id: str) -> bool: """Remove a registered handler. WARNING: This is primarily for testing. Do not call in production code. Args: effect_id: The effect identifier to unregister. Returns: True if the handler was removed, False if it wasn't registered. """ if effect_id in _EFFECT_REGISTRY: del _EFFECT_REGISTRY[effect_id] return True return False