tabras / tests /test_game.py
vvennelakanti's picture
Build Tabras card duel prototype
6bbf552
Raw
History Blame Contribute Delete
8.34 kB
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"