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"