Spaces:
Sleeping
Sleeping
| from random import Random | |
| import pytest | |
| from budget import Card | |
| from game import ( | |
| DuelState, | |
| PendingEffect, | |
| advance_pending, | |
| begin_turn, | |
| create_player, | |
| deal_damage, | |
| draw_cards, | |
| named_player, | |
| play_card, | |
| play_card_from_hand, | |
| round_order, | |
| start_round, | |
| winner, | |
| ) | |
| from primitives import Effect | |
| # Build a test card with fixed effects. | |
| def make_card(cost: int, *effects: Effect) -> Card: | |
| return Card("Test", cost, "fire", "wuxia", tuple(effects)) | |
| # Build a duel state with empty decks. | |
| def make_duel() -> DuelState: | |
| return DuelState(create_player("player", []), create_player("enemy", [])) | |
| # Verify draw applies escalating fatigue when the deck is empty. | |
| def test_draw_cards_applies_fatigue() -> None: | |
| player = create_player("player", [make_card(1, Effect("deal", amount=2))]) | |
| draw_cards(player, 3) | |
| assert len(player.hand) == 1 | |
| assert player.hp == 17 | |
| assert player.fatigue == 3 | |
| # Verify damage resolves through block before HP. | |
| def test_damage_uses_block() -> None: | |
| state = make_duel() | |
| state.enemy.block = 3 | |
| dealt = deal_damage(state.player, state.enemy, 5) | |
| assert dealt == 2 | |
| assert state.enemy.block == 0 | |
| assert state.enemy.hp == 18 | |
| assert state.enemy.shield_charge == 1 | |
| # Verify ward absorbs chip and breaks on a single threshold hit. | |
| def test_ward_bubble_rule() -> None: | |
| state = make_duel() | |
| state.enemy.ward = 4 | |
| assert deal_damage(state.player, state.enemy, 3) == 0 | |
| assert state.enemy.ward == 4 | |
| assert state.enemy.hp == 20 | |
| assert state.enemy.shield_charge == 0 | |
| assert deal_damage(state.player, state.enemy, 4) == 0 | |
| assert state.enemy.ward == 0 | |
| assert state.enemy.hp == 20 | |
| assert state.enemy.shield_charge == 0 | |
| # Verify weak and vulnerable modify outgoing damage deterministically. | |
| def test_weak_and_vulnerable_modify_damage() -> None: | |
| state = make_duel() | |
| state.player.weak = 2 | |
| state.enemy.vulnerable = 3 | |
| assert deal_damage(state.player, state.enemy, 5) == 6 | |
| assert state.enemy.hp == 14 | |
| state.player.weak = 10 | |
| assert deal_damage(state.player, state.enemy, 5) == 0 | |
| # Verify playing a card spends energy and resolves effects. | |
| def test_play_card_spends_energy_and_resolves_effects() -> None: | |
| state = make_duel() | |
| state.player.energy = 3 | |
| play_card(state, state.player, make_card(2, Effect("deal", amount=4), Effect("block", amount=3))) | |
| assert state.player.energy == 1 | |
| assert state.player.block == 3 | |
| assert state.enemy.hp == 16 | |
| with pytest.raises(ValueError, match="not enough"): | |
| play_card(state, state.player, make_card(2, Effect("deal", amount=2))) | |
| # Verify playing from hand removes the card. | |
| def test_play_card_from_hand() -> None: | |
| state = make_duel() | |
| state.player.energy = 1 | |
| state.player.hand.append(make_card(1, Effect("deal", amount=2))) | |
| card = play_card_from_hand(state, state.player, 0) | |
| assert card.name == "Test" | |
| assert state.player.hand == [] | |
| assert state.enemy.hp == 18 | |
| # Verify all non-damage card effects update state. | |
| def test_card_effects_update_state() -> None: | |
| state = make_duel() | |
| state.player.deck = [make_card(1, Effect("deal", amount=2))] | |
| state.player.energy = 5 | |
| play_card( | |
| state, | |
| state.player, | |
| make_card( | |
| 1, | |
| Effect("ward", amount=2), | |
| Effect("weak", amount=2, duration=1), | |
| Effect("draw", amount=1), | |
| Effect("energy", amount=2), | |
| Effect("initiative"), | |
| Effect("vulnerable", amount=1, duration=1), | |
| ), | |
| ) | |
| assert state.player.ward == 2 | |
| assert state.player.energy == 6 | |
| assert len(state.player.hand) == 1 | |
| assert state.enemy.weak == 2 | |
| assert state.enemy.vulnerable == 1 | |
| assert state.forced_second == "enemy" | |
| # Verify multi-hit chip cannot break ward. | |
| def test_multi_hit_chip_cannot_break_ward() -> None: | |
| state = make_duel() | |
| state.enemy.ward = 3 | |
| state.player.energy = 1 | |
| play_card(state, state.player, make_card(1, Effect("multi_hit", amount=2, hits=2))) | |
| assert state.enemy.ward == 3 | |
| assert state.enemy.hp == 20 | |
| # Verify conditional and scaling damage use current state. | |
| def test_conditional_and_scaling_damage() -> None: | |
| state = make_duel() | |
| state.enemy.hp = 10 | |
| state.player.energy = 2 | |
| play_card(state, state.player, make_card(1, Effect("conditional", amount=6), Effect("scaling", amount=1))) | |
| assert state.enemy.hp == 5 | |
| # Verify shield charge scaling releases absorbed damage as burst. | |
| def test_shield_charge_scaling_spends_charge() -> None: | |
| state = make_duel() | |
| state.player.shield_charge = 4 | |
| state.player.energy = 1 | |
| play_card(state, state.player, make_card(1, Effect("scaling", amount=2, condition="shield_charge"))) | |
| assert state.enemy.hp == 14 | |
| assert state.player.shield_charge == 0 | |
| # Verify burn and bomb use the shared pending effect queue. | |
| def test_deferred_effects_advance_from_one_queue() -> None: | |
| state = make_duel() | |
| state.player.energy = 2 | |
| play_card(state, state.player, make_card(1, Effect("burn", amount=2, duration=2), Effect("bomb", amount=5, delay=1))) | |
| assert len(state.pending) == 2 | |
| advance_pending(state) | |
| assert state.enemy.hp == 20 | |
| advance_pending(state) | |
| assert state.enemy.hp == 13 | |
| assert len(state.pending) == 1 | |
| advance_pending(state) | |
| assert state.enemy.hp == 11 | |
| assert state.pending == [] | |
| # Verify start_round refills energy, draws, ticks statuses, and returns order. | |
| def test_start_round_updates_both_players() -> None: | |
| state = DuelState( | |
| create_player("player", [make_card(1, Effect("deal", amount=2))]), | |
| create_player("enemy", [make_card(1, Effect("deal", amount=2))]), | |
| ) | |
| state.player.vulnerable = 2 | |
| state.player.vulnerable_turns = 1 | |
| order = start_round(state, Random(1)) | |
| assert order in (("player", "enemy"), ("enemy", "player")) | |
| assert state.round_number == 1 | |
| assert state.player.energy == 1 | |
| assert state.player.vulnerable == 0 | |
| assert len(state.player.hand) == 1 | |
| assert len(state.enemy.hand) == 1 | |
| # Verify multi-turn statuses decrement without clearing early. | |
| def test_begin_turn_keeps_multi_turn_statuses() -> None: | |
| player = create_player("player", []) | |
| player.vulnerable = 2 | |
| player.vulnerable_turns = 2 | |
| player.weak = 1 | |
| player.weak_turns = 2 | |
| begin_turn(player, 3) | |
| assert player.vulnerable == 2 | |
| assert player.vulnerable_turns == 1 | |
| assert player.weak == 1 | |
| assert player.weak_turns == 1 | |
| # Verify one-turn weak clears at turn start. | |
| def test_begin_turn_clears_one_turn_weak() -> None: | |
| player = create_player("player", []) | |
| player.weak = 3 | |
| player.weak_turns = 1 | |
| begin_turn(player, 2) | |
| assert player.weak == 0 | |
| assert player.weak_turns == 0 | |
| # Verify initiative forces the named side to act second once. | |
| def test_round_order_respects_forced_second_once() -> None: | |
| state = make_duel() | |
| state.forced_second = "enemy" | |
| assert round_order(state, Random(1)) == ("player", "enemy") | |
| assert state.forced_second is None | |
| state.forced_second = "player" | |
| assert round_order(state, Random(1)) == ("enemy", "player") | |
| # Verify conditional effects do nothing when their condition is false. | |
| def test_conditional_does_not_fire_above_half_hp() -> None: | |
| state = make_duel() | |
| state.player.energy = 1 | |
| play_card(state, state.player, make_card(1, Effect("conditional", amount=6))) | |
| assert state.enemy.hp == 20 | |
| # Verify an already-due bomb resolves directly from the pending queue. | |
| def test_due_bomb_resolves_from_pending_queue() -> None: | |
| state = make_duel() | |
| state.pending.append(PendingEffect("bomb", "player", "enemy", 5)) | |
| advance_pending(state) | |
| assert state.enemy.hp == 15 | |
| assert state.pending == [] | |
| # Verify named player lookup and winner states. | |
| def test_named_player_and_winner() -> None: | |
| state = make_duel() | |
| assert named_player(state, "player") is state.player | |
| assert named_player(state, "enemy") is state.enemy | |
| with pytest.raises(KeyError): | |
| named_player(state, "missing") | |
| assert winner(state) is None | |
| state.enemy.hp = 0 | |
| assert winner(state) == "player" | |
| state.player.hp = 0 | |
| assert winner(state) == "draw" | |
| state.enemy.hp = 1 | |
| assert winner(state) == "enemy" | |