Effects System (Week 3): - EffectContext: helper methods for player/card access, params, coin flips - EffectResult: success, message, effect_type, details for logging - @effect_handler decorator with sync/async support and introspection - resolve_effect() for executing effects by ID Built-in Handlers (13 total): - deal_damage: raw damage primitive (poison, burn, recoil) - attack_damage: combat damage with modifiers, weakness, resistance - heal, draw_cards, discard_from_hand, shuffle_deck - apply_status, remove_status - coin_flip_damage, bench_damage - discard_energy, modify_hp, modify_retreat_cost Configurable Weakness/Resistance: - ModifierMode enum: MULTIPLICATIVE (x2) or ADDITIVE (+20) - CombatConfig in RulesConfig for game-wide defaults - WeaknessResistance supports per-card mode/value overrides - Legacy 'modifier' field maintained for backwards compatibility Test Coverage: 98% (418 tests) - 84 tests for effects system (base, registry, handlers) - Comprehensive edge case coverage for all handlers - CardDefinition helper methods tested for non-Pokemon cards - Zone edge cases (draw_bottom empty, peek_bottom overflow)
183 lines
5.5 KiB
Python
183 lines
5.5 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.
|
|
"""
|
|
|
|
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
|