tabras / tests /test_play.py
vvennelakanti's picture
Build Tabras card duel prototype
6bbf552
Raw
History Blame Contribute Delete
12 kB
from random import Random
from budget import Card
from game import DuelState, create_player
from play import (
best_pack_index,
best_draft_index,
card_line,
card_primitives,
choose_assistant_card,
choose_enemy_card,
choice_state_line,
combat_card_score,
demo_deck,
draft_enemy_deck,
draft_card_score,
draft_player_deck,
ice_need_bonus,
earth_need_bonus,
effect_score,
immediate_hp_damage,
pack_line,
playable_indexes,
player_line,
play_turn,
prompt_pack_choice,
prompt_human_choice,
run_text_duel,
shuffled_deck,
)
from primitives import Effect
# Build a play-loop test card.
def make_card(name: str, cost: int, effect: Effect) -> Card:
return Card(name, cost, "fire", "wuxia", (effect,))
# Build a play-loop test duel.
def make_duel() -> DuelState:
return DuelState(create_player("You", []), create_player("Enemy", []))
class PlayPackClient:
# Initialize call tracking for play draft generation.
def __init__(self) -> None:
self.calls = 0
# Return one legal fire pack.
def create_pack(self, payload: dict[str, object]) -> dict[str, object]:
self.calls += 1
if "costs" in payload:
costs = payload["costs"] # type: ignore[assignment]
return {
"cards": [
{"name": f"Spark {self.calls}-{index}", "flavor": "A", "effects": [{"primitive_id": "deal"}]}
for index, _ in enumerate(costs, 1)
]
}
return {
"cards": [
{"name": f"Spark {self.calls}A", "flavor": "A", "effects": [{"primitive_id": "deal"}]},
{"name": f"Spark {self.calls}B", "flavor": "B", "effects": [{"primitive_id": "burn"}]},
{"name": f"Spark {self.calls}C", "flavor": "C", "effects": [{"primitive_id": "scaling"}]},
]
}
# Verify card and player display lines stay compact.
def test_display_lines() -> None:
card = make_card("Spark", 1, Effect("deal", amount=2))
player = create_player("You", [])
player.energy = 1
player.block = 2
player.ward = 3
assert card_line(0, card) == "0: Spark [1] - Deal 2 damage."
assert player_line(player) == "You: 20 HP, 1 energy, 2 block, 3 ward, 0 charge (hand 0, deck 0, discard 0)"
# Verify choice state shows the active energy and target defenses.
def test_choice_state_line() -> None:
state = make_duel()
state.player.energy = 3
state.enemy.hp = 12
state.enemy.block = 2
state.enemy.ward = 1
assert choice_state_line(state, state.player) == "You energy 3. Enemy: 12 HP, 2 block, 1 ward."
# Verify draft pack lines include flavor.
def test_pack_line() -> None:
card = Card("Spark", 1, "fire", "wuxia", (Effect("deal", amount=2),), flavor="Hot.")
assert pack_line(0, card) == "0: Spark [1] - Deal 2 damage. | Hot."
# Verify playable indexes filter by current energy.
def test_playable_indexes() -> None:
player = create_player("You", [])
player.energy = 2
player.hand = [
make_card("One", 1, Effect("deal", amount=2)),
make_card("Three", 3, Effect("deal", amount=6)),
]
assert playable_indexes(player) == (0,)
# Verify the enemy chooses lethal before other playable cards.
def test_choose_enemy_card_prefers_lethal() -> None:
state = make_duel()
state.enemy.energy = 3
state.player.hp = 2
state.enemy.hand = [
make_card("Cheap", 1, Effect("deal", amount=2)),
make_card("Big", 3, Effect("block", amount=10)),
]
assert choose_enemy_card(state, state.enemy) == 0
assert choose_assistant_card(state, state.enemy) == 0
# Verify the enemy passes without playable cards.
def test_choose_enemy_card_passes_without_playable_cards() -> None:
state = make_duel()
state.enemy.energy = 0
state.enemy.hand = [make_card("Big", 3, Effect("deal", amount=4))]
assert choose_assistant_card(state, state.enemy) is None
# Verify the enemy values defense when behind.
def test_choose_enemy_card_values_defense_when_behind() -> None:
state = make_duel()
state.enemy.hp = 5
state.player.hp = 12
state.enemy.energy = 2
state.enemy.hand = [
make_card("Small hit", 1, Effect("deal", amount=2)),
make_card("Guard", 2, Effect("block", amount=6)),
]
assert choose_enemy_card(state, state.enemy) == 1
# Verify human input accepts pass and retries invalid choices.
def test_prompt_human_choice() -> None:
outputs: list[str] = []
answers = iter(["9", "0"])
state = make_duel()
state.player.energy = 1
state.player.hand = [make_card("Spark", 1, Effect("deal", amount=2))]
chooser = prompt_human_choice(lambda _: next(answers), outputs.append)
assert chooser(state, state.player) == 0
assert "That card is not playable." in outputs
assert "You energy 1. Enemy: 20 HP, 0 block, 0 ward." in outputs
pass_chooser = prompt_human_choice(lambda _: "pass", outputs.append)
assert pass_chooser(state, state.player) is None
# Verify draft prompt accepts one shown card and retries invalid input.
def test_prompt_pack_choice() -> None:
outputs: list[str] = []
answers = iter(["5", "1"])
pack = (
make_card("A", 1, Effect("deal", amount=2)),
make_card("B", 1, Effect("deal", amount=2)),
make_card("C", 1, Effect("deal", amount=2)),
)
chooser = prompt_pack_choice(lambda _: next(answers), outputs.append)
assert chooser((), pack) == 1
assert "Choose one of the shown draft cards." in outputs
# Verify player drafting produces a 15-card deck from nine packs.
def test_draft_player_deck() -> None:
outputs: list[str] = []
client = PlayPackClient()
deck = draft_player_deck(client, "fire", "dark fantasy", lambda _: "2", outputs.append)
assert len(deck) == 15
assert client.calls == 9
assert deck[-1].name == "Spark 9C"
assert "Draft pick 9/9. Current deck: 14 cards." in outputs
# Verify enemy drafting chooses the highest-scoring generated card.
def test_draft_enemy_deck() -> None:
outputs: list[str] = []
client = PlayPackClient()
deck = draft_enemy_deck(client, "fire", "dark fantasy", outputs.append, Random(0))
assert len(deck) == 15
assert client.calls == 9
assert any("Enemy generated pick 9/9" in line for line in outputs)
# Verify pack selection uses static card scoring.
def test_best_pack_index() -> None:
pack = (
make_card("Block", 1, Effect("block", amount=3)),
make_card("Hit", 1, Effect("deal", amount=4)),
make_card("Draw", 1, Effect("draw", amount=1)),
)
assert best_pack_index(pack) == 1
# Verify deck-aware draft selection can choose synergy over raw damage.
def test_best_draft_index_uses_school_needs() -> None:
current_deck = (Card("Setup", 1, "ice", "wuxia", (Effect("vulnerable", amount=1),)),)
pack = (
Card("Damage", 2, "ice", "wuxia", (Effect("deal", amount=8),)),
Card("Payoff", 2, "ice", "wuxia", (Effect("multi_hit", amount=1, hits=2),)),
)
assert best_draft_index(current_deck, pack) == 1
assert draft_card_score(current_deck, pack[1]) > draft_card_score(current_deck, pack[0])
# Verify Ice bonus rewards missing payoff.
def test_ice_need_bonus_rewards_missing_payoff() -> None:
assert ice_need_bonus(("multi_hit",), {"vulnerable": 1}) > 0
assert card_primitives(make_card("Hit", 1, Effect("deal", amount=1))) == ("deal",)
# Verify Earth bonus rewards block after scaling payoff.
def test_earth_need_bonus_rewards_block_engine() -> None:
assert earth_need_bonus(("block",), {"scaling": 2, "block": 1}) > earth_need_bonus(("deal",), {"scaling": 2, "block": 1})
# Verify tactical score helpers cover primitive categories.
def test_score_helpers() -> None:
state = make_duel()
state.enemy.hp = 5
state.player.hp = 10
card = make_card("Guard", 1, Effect("block", amount=3))
assert combat_card_score(state, state.enemy, card) > combat_card_score(state, state.player, card)
assert effect_score("multi_hit", 2, 3) == 12
assert effect_score("burn", 2, 1) == 4
assert effect_score("initiative", 0, 1) == 2
assert effect_score("unknown", 0, 1) == 0
# Verify immediate damage estimates cover tactical effect shapes.
def test_immediate_hp_damage_shapes() -> None:
state = make_duel()
state.enemy.block = 1
assert immediate_hp_damage(state.player, state.enemy, make_card("Hits", 1, Effect("multi_hit", amount=2, hits=2))) == 2
state.enemy.block = 0
state.enemy.hp = 10
assert immediate_hp_damage(state.player, state.enemy, make_card("Finish", 1, Effect("conditional", amount=3))) == 1
state.player.cards_played_this_turn = 1
assert immediate_hp_damage(state.player, state.enemy, make_card("Grow", 1, Effect("scaling", amount=2))) == 4
state.player.shield_charge = 5
assert immediate_hp_damage(state.player, state.enemy, make_card("Burst", 1, Effect("scaling", amount=2, condition="shield_charge"))) == 7
state.enemy.ward = 1
assert immediate_hp_damage(state.player, state.enemy, make_card("Stopped", 1, Effect("deal", amount=9))) == 0
state.enemy.ward = 0
state.player.weak = 9
assert immediate_hp_damage(state.player, state.enemy, make_card("Weak", 1, Effect("deal", amount=3))) == 0
# Verify play_turn resolves selected cards until pass.
def test_play_turn_resolves_cards() -> None:
outputs: list[str] = []
state = make_duel()
state.player.energy = 2
state.player.hand = [
make_card("Spark", 1, Effect("deal", amount=2)),
make_card("Guard", 1, Effect("block", amount=3)),
]
choices = iter([0, None])
play_turn(state, state.player, lambda _, __: next(choices), outputs.append)
assert state.enemy.hp == 18
assert state.player.hand[0].name == "Guard"
assert state.player.discard[0].name == "Spark"
assert outputs[-1] == "You passes."
# Verify the demo deck has the locked deck size.
def test_demo_deck_has_fifteen_cards() -> None:
deck = demo_deck("fire", "dark fantasy")
assert len(deck) == 15
assert deck[0].rules_text() == "Deal 2 damage."
# Verify deck shuffling is deterministic from the run RNG.
def test_shuffled_deck_uses_rng() -> None:
deck = tuple(make_card(str(index), 1, Effect("deal", amount=1)) for index in range(5))
shuffled = shuffled_deck(deck, Random(1))
assert [card.name for card in shuffled] != [card.name for card in deck]
assert [card.name for card in shuffled] == [card.name for card in shuffled_deck(deck, Random(1))]
# Verify a short scripted duel can produce a winner.
def test_run_text_duel_returns_winner() -> None:
outputs: list[str] = []
player_deck = tuple(make_card("Strike", 1, Effect("deal", amount=25)) for _ in range(4))
enemy_deck = (
make_card("Wait", 5, Effect("block", amount=1)),
make_card("Wait", 5, Effect("block", amount=1)),
make_card("Wait", 5, Effect("block", amount=1)),
make_card("Wait", 5, Effect("block", amount=1)),
)
result = run_text_duel(player_deck, enemy_deck, lambda _: "0", outputs.append, Random(0), max_rounds=1)
assert result == "You"
assert outputs[-1] == "Winner: You"
# Verify the round stops immediately when the first actor wins.
def test_run_text_duel_stops_after_first_actor_win() -> None:
outputs: list[str] = []
player_deck = tuple(make_card("Strike", 1, Effect("deal", amount=25)) for _ in range(4))
enemy_deck = (
make_card("Wait", 1, Effect("deal", amount=1)),
make_card("Wait", 1, Effect("deal", amount=1)),
make_card("Wait", 1, Effect("deal", amount=1)),
make_card("Wait", 1, Effect("deal", amount=1)),
)
result = run_text_duel(player_deck, enemy_deck, lambda _: "0", outputs.append, Random(3), max_rounds=1)
assert result == "You"
assert not any(line.startswith("Enemy plays") for line in outputs)