rabukasim / engine /game /effects /costs.py
trioskosmos's picture
Upload folder using huggingface_hub
463f868 verified
from typing import Any, Dict, List
from engine.game.enums import Phase
from engine.models.ability import AbilityCostType, Cost, EffectType
def can_pay_costs(player: Any, costs: List[Cost], source_area: int = -1, start_index: int = 0) -> bool:
"""Non-mutating check if a player can afford the costs."""
total_reduction = sum(
ce["effect"].value for ce in player.continuous_effects if ce["effect"].effect_type == EffectType.REDUCE_COST
)
for cost in costs[start_index:]:
if cost.params.get("cost_type_name") == "SELECT_SELF_OR_DISCARD":
can_tap_self = source_area >= 0 and not player.tapped_members[source_area]
can_discard = len(player.hand) > 0
if not (can_tap_self or can_discard):
return False
continue
if cost.type == AbilityCostType.ENERGY:
needed = max(0, cost.value - total_reduction)
if player.count_untapped_energy() < needed:
return False
elif cost.type == AbilityCostType.TAP_SELF:
if source_area < 0 or player.tapped_members[source_area]:
return False
elif cost.type in (AbilityCostType.SACRIFICE_SELF, AbilityCostType.RETURN_HAND):
if source_area < 0 or player.stage[source_area] < 0:
return False
elif cost.type == AbilityCostType.DISCARD_HAND:
if len(player.hand) < cost.value:
return False
elif cost.type == AbilityCostType.SACRIFICE_UNDER:
if source_area < 0 or not player.stage_energy[source_area]:
return False
elif cost.type == AbilityCostType.DISCARD_ENERGY:
if player.count_untapped_energy() < 1:
return False
return True
def pay_costs(game: Any, p: Any, costs: List[Cost], source_area: int = -1, start_index: int = 0) -> bool:
"""
Pay costs. Returns True if paid, False if cancelled/deferred (optional).
If optional cost is encountered, it queues a choice and returns False.
"""
if not can_pay_costs(p, costs, source_area, start_index):
return False
total_reduction = sum(
ce["effect"].value for ce in p.continuous_effects if ce["effect"].effect_type == EffectType.REDUCE_COST
)
# Default metadata for cost payment
scid = getattr(game, "current_resolving_member_id", -1)
choice_metadata: Dict[str, Any] = {"source_card_id": scid, "step_progress": "Cost"}
for i, cost in enumerate(costs[start_index:]):
cost_idx = start_index + i
if cost.params.get("cost_type_name") == "SELECT_SELF_OR_DISCARD":
game.pending_choices.append(
(
"SELECT_MODE",
{
**choice_metadata,
"cost_index": cost_idx,
"cost_type_name": "SELECT_SELF_OR_DISCARD",
"options": ["ウェイト", "手札を1枚控え室に置く"],
"effect_description": "このメンバーをウェイトにするか、手札を1枚控え室に置く",
"source_area": source_area,
},
)
)
game.phase = Phase.RESPONSE
return False
if cost.type == AbilityCostType.ENERGY:
if cost.is_optional:
if game.verbose:
print(f"DEBUG: Queueing PAY_COST_OPTIONAL for player {p.player_id}")
game.pending_choices.append(
(
"PAY_COST_OPTIONAL",
{
**choice_metadata,
"cost_index": cost_idx,
"amount": cost.value,
"cost_type": "energy",
"effect_description": f"Pay optional energy cost ({cost.value})",
},
)
)
game.phase = Phase.RESPONSE
return False
act = max(0, cost.value - total_reduction)
tapped = 0
for i in range(len(p.energy_zone) - 1, -1, -1):
if tapped >= act:
break
if not p.tapped_energy[i]:
p.tapped_energy[i] = True
tapped += 1
elif cost.type == AbilityCostType.TAP_SELF:
if source_area >= 0:
p.tapped_members[source_area] = True
elif cost.type == AbilityCostType.SACRIFICE_SELF:
if source_area >= 0 and p.stage[source_area] >= 0:
p.discard.append(p.stage[source_area])
p.stage[source_area] = -1
p.energy_deck.extend(p.stage_energy[source_area])
p.stage_energy[source_area] = []
p.tapped_members[source_area] = False
p.members_played_this_turn[source_area] = False
elif cost.type == AbilityCostType.SACRIFICE_UNDER:
if source_area >= 0:
p.energy_deck.extend(p.stage_energy[source_area])
p.stage_energy[source_area] = []
elif cost.type == AbilityCostType.DISCARD_ENERGY:
for i in range(len(p.energy_zone) - 1, -1, -1):
if not p.tapped_energy[i]:
p.tapped_energy[i] = True
break
elif cost.type == AbilityCostType.RETURN_HAND:
if source_area >= 0 and p.stage[source_area] >= 0:
p.hand.append(p.stage[source_area])
p.stage[source_area] = -1
p.energy_deck.extend(p.stage_energy[source_area])
p.stage_energy[source_area] = []
elif cost.type == AbilityCostType.DISCARD_HAND:
if cost.is_optional:
game.pending_choices.append(
(
"PAY_COST_OPTIONAL",
{
**choice_metadata,
"cost_index": cost_idx,
"amount": cost.value,
"cost_type": "discard",
"effect_description": f"Discard cards for optional cost ({cost.value})",
},
)
)
game.phase = Phase.RESPONSE
return False
params = {
"reason": "cost",
"effect": "discard",
"is_optional": cost.is_optional,
"cost_index": cost_idx,
"count": cost.value,
}
if hasattr(cost, "params") and cost.params:
params.update(cost.params)
game.pending_choices.append(("TARGET_HAND", {**choice_metadata, **params}))
return False
elif cost.type in (AbilityCostType.TAP_MEMBER, AbilityCostType.REST_MEMBER):
# Pending implementation of choices for these types (if any)
pass
return True