This commit is contained in:
2026-03-18 15:33:24 +01:00
parent 5e7a6808ab
commit 867c51062b
39 changed files with 6499 additions and 161 deletions

444
backend/test_game.py Normal file
View File

@@ -0,0 +1,444 @@
from game import (
GameState, PlayerState, CardInstance, CombatEvent, GameResult,
create_game, resolve_combat, check_win_condition,
action_play_card, action_sacrifice, action_end_turn,
BOARD_SIZE, HAND_SIZE, STARTING_LIFE, MAX_ENERGY_CAP,
)
import uuid
# ── Helpers ──────────────────────────────────────────────────────────────────
def make_card(name="Test Card", attack=10, defense=10, cost=2, **kwargs) -> CardInstance:
return CardInstance(
instance_id=str(uuid.uuid4()),
card_id=str(uuid.uuid4()),
name=name,
attack=attack,
defense=defense,
max_defense=defense,
cost=cost,
card_type="other",
card_rarity="common",
image_link="",
text="",
**kwargs,
)
def make_game(
p1_board=None, p2_board=None,
p1_hand=None, p2_hand=None,
p1_deck=None, p2_deck=None,
p1_life=STARTING_LIFE, p2_life=STARTING_LIFE,
p1_energy=6, p2_energy=6,
) -> GameState:
p1 = PlayerState(
user_id="p1",
username="player 1",
deck_type="test",
life=p1_life,
hand=p1_hand or [],
deck=p1_deck or [],
board=p1_board or [None] * BOARD_SIZE,
energy=p1_energy,
energy_cap=6,
)
p2 = PlayerState(
user_id="p2",
username="player 2",
deck_type="test",
life=p2_life,
hand=p2_hand or [],
deck=p2_deck or [],
board=p2_board or [None] * BOARD_SIZE,
energy=p2_energy,
energy_cap=6,
)
return GameState(
game_id="test-game",
players={"p1": p1, "p2": p2},
player_order=["p1", "p2"],
active_player_id="p1",
phase="main",
)
# ── create_game ───────────────────────────────────────────────────────────────
class TestCreateGame:
def test_both_players_present(self):
class FakeCard:
id = uuid.uuid4()
name = "Card"
attack = 5
defense = 5
cost = 1
card_type = "other"
card_rarity = "common"
image_link = ""
text = ""
cards = [FakeCard() for _ in range(20)]
state = create_game("p1", "player 1", "test", cards, "p2", "player 2", "test", cards)
assert "p1" in state.players
assert "p2" in state.players
def test_first_player_has_drawn(self):
class FakeCard:
id = uuid.uuid4()
name = "Card"
attack = 5
defense = 5
cost = 1
card_type = "other"
card_rarity = "common"
image_link = ""
text = ""
cards = [FakeCard() for _ in range(20)]
state = create_game("p1", "player 1", "test", cards, "p2", "player 2", "test", cards)
first = state.players[state.active_player_id]
assert len(first.hand) == HAND_SIZE
def test_second_player_has_not_drawn(self):
class FakeCard:
id = uuid.uuid4()
name = "Card"
attack = 5
defense = 5
cost = 1
card_type = "other"
card_rarity = "common"
image_link = ""
text = ""
cards = [FakeCard() for _ in range(20)]
state = create_game("p1", "player 1", "test", cards, "p2", "player 2", "test", cards)
second_id = state.opponent_id(state.active_player_id)
second = state.players[second_id]
assert len(second.hand) == 0
def test_first_player_starts_with_energy(self):
class FakeCard:
id = uuid.uuid4()
name = "Card"
attack = 5
defense = 5
cost = 1
card_type = "other"
card_rarity = "common"
image_link = ""
text = ""
cards = [FakeCard() for _ in range(20)]
state = create_game("p1", "player 1", "test", cards, "p2", "player 2", "test", cards)
first = state.players[state.active_player_id]
opp = state.players[state.opponent_id(state.active_player_id)]
assert first.energy == 1
assert first.energy_cap == 1
assert opp.energy == 0
assert opp.energy_cap == 1
# ── action_play_card ──────────────────────────────────────────────────────────
class TestPlayCard:
def test_play_card_successfully(self):
card = make_card(cost=2)
state = make_game(p1_hand=[card], p1_energy=3)
err = action_play_card(state, hand_index=0, slot=0)
assert err is None
assert state.players["p1"].board[0] is card
assert state.players["p1"].energy == 1
assert card not in state.players["p1"].hand
def test_play_card_multiple(self):
card1 = make_card(cost=2)
card2 = make_card(cost=1)
state = make_game(p1_hand=[card1,card2], p1_energy=3)
err1 = action_play_card(state, hand_index=0, slot=0)
err2 = action_play_card(state, hand_index=0, slot=1)
assert err1 is None
assert err2 is None
assert state.players["p1"].board[0] is card1
assert state.players["p1"].board[1] is card2
assert state.players["p1"].energy == 0
assert card1 not in state.players["p1"].hand
assert card2 not in state.players["p1"].hand
def test_play_card_not_enough_energy(self):
card = make_card(cost=5)
state = make_game(p1_hand=[card], p1_energy=2)
err = action_play_card(state, hand_index=0, slot=0)
assert err is not None
assert err == "Not enough energy (have 2, need 5)"
assert state.players["p1"].board[0] is None
def test_play_card_slot_occupied(self):
existing = make_card(name="Existing")
new_card = make_card(name="New")
board = [existing] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=board, p1_hand=[new_card], p1_energy=6)
err = action_play_card(state, hand_index=0, slot=0)
assert err is not None
assert err == "Slot already occupied"
assert state.players["p1"].board[0] is existing
def test_play_card_invalid_slot(self):
card = make_card()
state = make_game(p1_hand=[card], p1_energy=6)
err = action_play_card(state, hand_index=0, slot=99)
assert err is not None
assert err == "Invalid slot 99"
def test_play_card_invalid_hand_index(self):
state = make_game(p1_hand=[], p1_energy=6)
err = action_play_card(state, hand_index=0, slot=0)
assert err is not None
assert err == "Invalid hand index"
def test_play_card_wrong_phase(self):
card = make_card()
state = make_game(p1_hand=[card], p1_energy=6)
state.phase = "combat"
err = action_play_card(state, hand_index=0, slot=0)
assert err is not None
assert err == "Not in main phase"
def test_play_card_exact_energy(self):
card = make_card(cost=3)
state = make_game(p1_hand=[card], p1_energy=3)
err = action_play_card(state, hand_index=0, slot=0)
assert err is None
assert state.players["p1"].energy == 0
# ── action_sacrifice ──────────────────────────────────────────────────────────
class TestSacrifice:
def test_sacrifice_returns_energy(self):
card = make_card(cost=3)
board = [card] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=board, p1_energy=1)
err = action_sacrifice(state, slot=0)
assert err is None
assert state.players["p1"].board[0] is None
assert state.players["p1"].energy == 4
def test_sacrifice_empty_slot(self):
state = make_game(p1_energy=3)
err = action_sacrifice(state, slot=0)
assert err is not None
assert err == "No card in that slot"
def test_sacrifice_invalid_slot(self):
state = make_game(p1_energy=3)
err = action_sacrifice(state, slot=99)
assert err is not None
assert err == "Invalid slot 99"
def test_sacrifice_wrong_phase(self):
card = make_card(cost=3)
board = [card] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=board, p1_energy=1)
state.phase = "combat"
err = action_sacrifice(state, slot=0)
assert err is not None
assert err == "Not in main phase"
def test_sacrifice_then_replay(self):
cheap = make_card(name="Cheap", cost=3)
expensive = make_card(name="Expensive", cost=5)
board = [None] + [cheap] + [None] * (BOARD_SIZE - 2)
state = make_game(p1_board=board, p1_hand=[expensive], p1_energy=2)
err1 = action_play_card(state, hand_index=0, slot=0)
assert err1 is not None
assert err1 == "Not enough energy (have 2, need 5)"
action_sacrifice(state, slot=1)
assert state.players["p1"].energy == 5
err2 = action_play_card(state, hand_index=0, slot=0)
assert err2 is None
# ── resolve_combat ────────────────────────────────────────────────────────────
class TestResolveCombat:
def test_uncontested_attack_hits_life(self):
attacker = make_card(attack=5)
board = [attacker] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=board)
events = resolve_combat(state)
assert state.players["p2"].life == STARTING_LIFE - 5
assert len(events) == 1
assert events[0].life_damage == 5
assert events[0].defender_slot is None
def test_contested_attack_reduces_defense(self):
attacker = make_card(attack=4)
defender = make_card(defense=10)
p1_board = [attacker] + [None] * (BOARD_SIZE - 1)
p2_board = [defender] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board, p2_board=p2_board)
events = resolve_combat(state)
assert defender.defense == 6
assert state.players["p2"].life == STARTING_LIFE
assert events[0].life_damage == 0
assert not events[0].defender_destroyed
def test_attack_destroys_defender(self):
attacker = make_card(attack=10)
defender = make_card(defense=5)
p1_board = [attacker] + [None] * (BOARD_SIZE - 1)
p2_board = [defender] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board, p2_board=p2_board)
events = resolve_combat(state)
assert state.players["p2"].board[0] is None
assert events[0].defender_destroyed is True
def test_no_overflow_damage(self):
attacker = make_card(attack=100)
defender = make_card(defense=5)
p1_board = [attacker] + [None] * (BOARD_SIZE - 1)
p2_board = [defender] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board, p2_board=p2_board)
resolve_combat(state)
assert state.players["p2"].life == STARTING_LIFE
def test_multiple_attackers(self):
attackers = [make_card(attack=3) for _ in range(3)]
p1_board = attackers + [None, None]
state = make_game(p1_board=p1_board)
resolve_combat(state)
assert state.players["p2"].life == STARTING_LIFE - 9
def test_empty_board_no_events(self):
state = make_game()
events = resolve_combat(state)
assert events == []
assert state.players["p2"].life == STARTING_LIFE
def test_exact_defense_destroys(self):
attacker = make_card(attack=5)
defender = make_card(defense=5)
p1_board = [attacker] + [None] * (BOARD_SIZE - 1)
p2_board = [defender] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board, p2_board=p2_board)
events = resolve_combat(state)
assert state.players["p2"].board[0] is None
assert events[0].defender_destroyed is True
# ── check_win_condition ───────────────────────────────────────────────────────
class TestWinCondition:
def test_no_win_initially(self):
state = make_game()
assert check_win_condition(state) is None
def test_p2_life_zero(self):
state = make_game(p2_life=0)
result = check_win_condition(state)
assert result is not None
assert result.winner_id == "p1"
assert result.reason == "Life reduced to zero"
def test_p1_life_zero(self):
state = make_game(p1_life=0)
result = check_win_condition(state)
assert result is not None
assert result.winner_id == "p2"
assert result.reason == "Life reduced to zero"
def test_p1_life_negative(self):
state = make_game(p1_life=-5)
result = check_win_condition(state)
assert result is not None
assert result.winner_id == "p2"
assert result.reason == "Life reduced to zero"
def test_win_by_no_cards(self):
card = make_card()
p1_board = [card] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board, p2_hand=[], p2_deck=[])
result = check_win_condition(state)
assert result is not None
assert result.winner_id == "p1"
assert result.reason == "Opponent has no playable cards remaining"
def test_no_win_if_p2_has_cards_in_hand(self):
card_on_board = make_card()
card_in_hand = make_card()
p1_board = [card_on_board] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board, p2_hand=[card_in_hand])
assert check_win_condition(state) is None
def test_no_win_if_attacker_has_no_board(self):
"""p1 has nothing on board even though p2 is empty."""
state = make_game(p2_hand=[], p2_deck=[])
assert check_win_condition(state) is None
# ── action_end_turn ───────────────────────────────────────────────────────────
class TestEndTurn:
def test_switches_active_player(self):
state = make_game()
action_end_turn(state)
assert state.active_player_id == "p2"
def test_next_player_draws(self):
deck = [make_card() for _ in range(10)]
state = make_game(p2_deck=deck)
action_end_turn(state)
assert len(state.players["p2"].hand) == HAND_SIZE
def test_next_player_energy_increments(self):
state = make_game()
state.players["p2"].energy_cap = 2
action_end_turn(state)
assert state.players["p2"].energy_cap == 3
assert state.players["p2"].energy == 3
def test_energy_cap_maxes_out(self):
state = make_game()
state.players["p2"].energy_cap = MAX_ENERGY_CAP
action_end_turn(state)
assert state.players["p2"].energy_cap == MAX_ENERGY_CAP
def test_combat_events_recorded(self):
attacker = make_card(attack=5)
p1_board = [attacker] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board)
action_end_turn(state)
assert len(state.last_combat_events) == 1
def test_game_ends_on_lethal(self):
attacker = make_card(attack=STARTING_LIFE)
p1_board = [attacker] + [None] * (BOARD_SIZE - 1)
state = make_game(p1_board=p1_board)
action_end_turn(state)
assert state.result is not None
assert state.result.winner_id == "p1"
assert state.phase == "end"
def test_wrong_phase_returns_error(self):
state = make_game()
state.phase = "combat"
err = action_end_turn(state)
assert err is not None
def test_draw_partial_when_deck_small(self):
deck = [make_card() for _ in range(2)]
state = make_game(p2_deck=deck)
action_end_turn(state)
assert len(state.players["p2"].hand) == 2
def test_full_turn_cycle(self):
card = make_card(cost=1)
deck = [make_card() for _ in range(10)]
state = make_game(p1_hand=[card], p2_deck=deck)
action_play_card(state, hand_index=0, slot=0)
action_end_turn(state)
assert state.active_player_id == "p2"
assert len(state.players["p2"].hand) == HAND_SIZE
assert state.phase == "main"