Effect handler exceptions now logged at ERROR level with full context: - effect_id, source_player_id, source/target card IDs, params - Full traceback via logger.exception() Game still returns safe EffectResult.failure() to prevent crashes, but debugging information is now preserved in logs.
197 lines
6.0 KiB
Python
197 lines
6.0 KiB
Python
"""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.
|
|
"""
|
|
|
|
import logging
|
|
from collections.abc import Callable
|
|
|
|
from app.core.effects.base import EffectContext, EffectResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 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:
|
|
# Log full traceback for debugging, but return safe failure result
|
|
# to prevent game crashes. This preserves the exception details
|
|
# while allowing the game to continue gracefully.
|
|
logger.exception(
|
|
"Effect handler '%s' raised an exception. Context: source_player=%s, "
|
|
"source_card=%s, target_card=%s, params=%s",
|
|
effect_id,
|
|
ctx.source_player_id,
|
|
ctx.source_card_id,
|
|
ctx.target_card_id,
|
|
ctx.params,
|
|
)
|
|
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
|