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"