tabras / tests /test_budget.py
vvennelakanti's picture
Build Tabras card duel prototype
6bbf552
Raw
History Blame Contribute Delete
3.85 kB
from random import Random
import pytest
from budget import CardSpec, EffectPlan, budget_for_energy, cost_card, cost_effect, cost_effects, miracle_budget
class MaxRoll:
# Return the largest requested miracle multiplier.
def randint(self, low: int, high: int) -> int:
assert (low, high) == (1, 3)
return high
# Verify energy cost maps directly to point budget.
def test_budget_for_energy_accepts_locked_range() -> None:
assert [budget_for_energy(cost) for cost in range(1, 6)] == [1, 2, 3, 4, 5]
with pytest.raises(ValueError, match="card cost"):
budget_for_energy(0)
with pytest.raises(ValueError, match="card cost"):
budget_for_energy(6)
# Verify miracle budgets are high variance but hard capped.
def test_miracle_budget_is_capped() -> None:
assert miracle_budget(5, MaxRoll()) == 8 # type: ignore[arg-type]
assert miracle_budget(2, Random(1)) <= 6
# Verify model-selected shapes become engine-owned numbers.
def test_cost_card_assigns_numbers_from_budget() -> None:
spec = CardSpec("Cinder", 3, "fire", "wuxia", (EffectPlan("deal"),), flavor="Hot.")
card = cost_card(spec)
assert card.effects[0].amount == 6
assert card.rules_text() == "Deal 6 damage."
# Verify Earth scaling releases shield charge.
def test_earth_scaling_uses_shield_charge() -> None:
spec = CardSpec("Stone Burst", 3, "earth", "wuxia", (EffectPlan("scaling"),))
card = cost_card(spec)
assert card.effects[0].condition == "shield_charge"
assert card.rules_text() == "Deal 3 damage plus your banked shield charge, then empty it."
# Verify weighted multi-effect cards spend the full budget.
def test_cost_effects_split_weighted_budget() -> None:
effects = cost_effects(4, (EffectPlan("deal", weight=1), EffectPlan("burn", weight=3)))
assert effects[0].amount == 1
assert effects[1].amount == 2
assert effects[1].duration == 2
# Verify low-cost cards cannot keep more effect clauses than they can fund.
def test_cost_effects_trims_overstuffed_plans() -> None:
effects = cost_effects(2, (EffectPlan("deal"), EffectPlan("burn"), EffectPlan("bomb")))
assert len(effects) == 2
assert [effect.primitive_id for effect in effects] == ["deal", "burn"]
@pytest.mark.parametrize(
("primitive_id", "points", "fields"),
(
("deal", 2, {"amount": 4}),
("burn", 4, {"amount": 4, "duration": 2}),
("bomb", 4, {"amount": 8, "delay": 3}),
("block", 2, {"amount": 6}),
("ward", 2, {"amount": 2}),
("weak", 2, {"amount": 4, "duration": 1}),
("draw", 3, {"amount": 2}),
("energy", 3, {"amount": 1}),
("initiative", 2, {"duration": 1}),
("multi_hit", 4, {"amount": 4, "hits": 3}),
("vulnerable", 4, {"amount": 4, "duration": 2}),
("conditional", 2, {"amount": 2, "condition": "opponent_below_half"}),
("scaling", 2, {"amount": 2, "condition": "cards_played"}),
),
)
# Verify every primitive has a costing rule.
def test_cost_effect_rules(primitive_id: str, points: int, fields: dict[str, object]) -> None:
effect = cost_effect(primitive_id, points) # type: ignore[arg-type]
for name, value in fields.items():
assert getattr(effect, name) == value
# Verify invalid effect plans are rejected before costing.
def test_cost_card_rejects_invalid_specs() -> None:
with pytest.raises(ValueError, match="at least one"):
cost_card(CardSpec("Blank", 1, "fire", "wuxia", ()))
with pytest.raises(ValueError, match="weight"):
cost_card(CardSpec("Bad", 1, "fire", "wuxia", (EffectPlan("deal", 0),)))
with pytest.raises(ValueError, match="not allowed"):
cost_card(CardSpec("Bad", 1, "ice", "wuxia", (EffectPlan("bomb"),)))
with pytest.raises(ValueError, match="positive"):
cost_effect("deal", 0)