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)