import pytest from fastapi.testclient import TestClient from app.main import app from app.game import game_engine @pytest.fixture(autouse=True) def reset_board_state(): import asyncio asyncio.run(game_engine.reset()) yield asyncio.run(game_engine.reset()) def test_party_formation_and_group_movement(): client = TestClient(app) p1 = client.post("/api/players", json={"name": "LeaderBot", "color": "#00FFAA"}).json() p2 = client.post("/api/players", json={"name": "FollowerBot", "color": "#FF00AA"}).json() # Place adjacent: Leader at (10, 10), Follower at (10, 11) (within distance 1) import asyncio async def place_adjacent(): b1 = await game_engine.get_player(p1["id"]) b1.x, b1.y = 10, 10 b2 = await game_engine.get_player(p2["id"]) b2.x, b2.y = 10, 11 asyncio.run(place_adjacent()) # Form party with agreed leader p1 form_res = client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"]], "leader_id": p1["id"], "name": "AlphaSquad", }) assert form_res.status_code == 201 party = form_res.json() assert party["leader_id"] == p1["id"] assert len(party["member_ids"]) == 2 # Follower cannot move individually res_follow = client.post(f"/api/players/{p2['id']}/move", json={"direction": "UP"}) assert res_follow.status_code == 403 assert "Only party leader" in res_follow.json()["detail"] # Leader moves UP: Leader moves from (10,10)->(10,9), Follower moves from (10,11)->(10,10) res_move = client.post(f"/api/players/{p1['id']}/move", json={"direction": "UP"}) assert res_move.status_code == 200 move_data = res_move.json() assert move_data["party_moved"] is True assert len(move_data["affected_players"]) == 2 # Check positions in board board = client.get("/api/board").json() bots_by_id = {b["id"]: b for b in board["players"]} assert bots_by_id[p1["id"]]["x"] == 10 assert bots_by_id[p1["id"]]["y"] == 9 assert bots_by_id[p2["id"]]["x"] == 10 assert bots_by_id[p2["id"]]["y"] == 10 def test_party_wall_collision_for_follower(): client = TestClient(app) p1 = client.post("/api/players", json={"name": "Leader", "color": "#111111"}).json() p2 = client.post("/api/players", json={"name": "Follower", "color": "#222222"}).json() # Place Leader at (5, 1), Follower at (5, 0) # Moving UP would push Follower to (5, -1) which hits the wall! import asyncio async def setup_near_wall(): b1 = await game_engine.get_player(p1["id"]) b1.x, b1.y = 5, 1 b2 = await game_engine.get_player(p2["id"]) b2.x, b2.y = 5, 0 asyncio.run(setup_near_wall()) client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"]], "leader_id": p1["id"], }) # Available moves check should mark UP as blocked due to follower hitting wall moves = client.get(f"/api/players/{p1['id']}/available-moves").json() assert moves["moves"]["UP"]["available"] is False assert "boundary wall" in moves["moves"]["UP"]["reason"] # Trying to move UP should fail res = client.post(f"/api/players/{p1['id']}/move", json={"direction": "UP"}) assert res.status_code == 400 assert "Illegal move" in res.json()["detail"] def test_party_defeat_and_leader_respawn(): client = TestClient(app) p1 = client.post("/api/players", json={"name": "LeaderToDie", "color": "#AA0000"}).json() p2 = client.post("/api/players", json={"name": "Survivor1", "color": "#00AA00"}).json() p3 = client.post("/api/players", json={"name": "Survivor2", "color": "#0000AA"}).json() import asyncio async def place_trio(): b1 = await game_engine.get_player(p1["id"]) b1.x, b1.y = 20, 20 b2 = await game_engine.get_player(p2["id"]) b2.x, b2.y = 20, 21 b3 = await game_engine.get_player(p3["id"]) b3.x, b3.y = 21, 21 asyncio.run(place_trio()) party = client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"], p3["id"]], "leader_id": p1["id"], }).json() # Trigger defeat on party defeat_res = client.post(f"/api/parties/{party['id']}/defeat") assert defeat_res.status_code == 200 data = defeat_res.json() # 1. Leader was killed, score is -1 assert data["killed_leader_id"] == p1["id"] assert data["killed_leader_new_score"] == -1 # 2. Leader respawned position is recorded assert "x" in data["killed_leader_respawn_position"] # 3. New leader randomly assigned from remaining members assert data["new_leader_id"] in [p2["id"], p3["id"]] assert len(data["remaining_members"]) == 2 # Verify leader is no longer part of the party dead_leader = client.get(f"/api/players/{p1['id']}").json() assert dead_leader["party_id"] is None assert dead_leader["is_party_leader"] is False assert dead_leader["score"] == -1 # Verify new leader in party updated_party = client.get(f"/api/parties/{party['id']}").json() assert updated_party["leader_id"] == data["new_leader_id"] assert p1["id"] not in updated_party["member_ids"] def test_battle_between_parties(): client = TestClient(app) # Party 1 has 2 bots p1 = client.post("/api/players", json={"name": "Warrior1", "color": "#111111"}).json() p2 = client.post("/api/players", json={"name": "Warrior2", "color": "#222222"}).json() # Party 2 has 1 solo bot p3 = client.post("/api/players", json={"name": "SoloDefender", "color": "#333333"}).json() import asyncio async def place_combatants(): b1 = await game_engine.get_player(p1["id"]) b1.x, b1.y = 30, 30 b2 = await game_engine.get_player(p2["id"]) b2.x, b2.y = 30, 31 b3 = await game_engine.get_player(p3["id"]) b3.x, b3.y = 31, 30 # adjacent to b1 asyncio.run(place_combatants()) client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"]], "leader_id": p1["id"], }) # Initiate battle: 2-bot party vs 1-bot solo -> Party 1 wins res = client.post("/api/battles/fight", json={ "challenger_id": p1["id"], "defender_id": p3["id"], }) assert res.status_code == 200 battle = res.json() assert battle["winner_id"] == p1["id"] assert battle["loser_id"] == p3["id"] # Check loser score = -1 loser_bot = client.get(f"/api/players/{p3['id']}").json() assert loser_bot["score"] == -1 # Check winner score = +1 winner_bot = client.get(f"/api/players/{p1['id']}").json() assert winner_bot["score"] == 1