import pytest import asyncio from fastapi.testclient import TestClient from app.main import app from app.game import game_engine @pytest.fixture(autouse=True) def reset_game_state(): client = TestClient(app) client.post("/api/reset") yield client.post("/api/reset") def test_strength_based_leadership_negotiation(): client = TestClient(app) # Register BotStrong with strength 5 and BotWeak with strength 2 b1 = client.post("/api/players", json={"name": "BotStrong", "color": "#FF0000", "strength": 5}).json() b2 = client.post("/api/players", json={"name": "BotWeak", "color": "#0000FF", "strength": 2}).json() # Place them adjacent to each other async def place(): p1 = await game_engine.get_player(b1["id"]) p1.x, p1.y = 10, 10 p2 = await game_engine.get_player(b2["id"]) p2.x, p2.y = 10, 11 asyncio.run(place()) # Step AI turn for the first player in turn order turn_info = client.get("/api/turn").json() acting_id = turn_info["current_player_id"] step_res = client.post(f"/api/players/{acting_id}/ai-step") assert step_res.status_code == 200 data = step_res.json() # Formed party should have occurred assert data["action_taken"] == "formed_party" party = data["formed_party"] assert party is not None # The stronger bot (BotStrong, strength 5) must insist on being the leader! assert party["leader_id"] == b1["id"] assert party["leader_name"] == "BotStrong" assert party["total_strength"] == 7 # 5 + 2 def test_bot_location_memory_and_radar(): client = TestClient(app) # Register BotAlpha and BotBravo b1 = client.post("/api/players", json={"name": "BotAlpha", "color": "#FF0000", "strength": 1}).json() b2 = client.post("/api/players", json={"name": "BotBravo", "color": "#00FF00", "strength": 1}).json() # Move BotAlpha manually async def set_loc(): p1 = await game_engine.get_player(b1["id"]) p1.x, p1.y = 5, 5 p1.visited_locations = [{"x": 5, "y": 5}] p2 = await game_engine.get_player(b2["id"]) p2.x, p2.y = 5, 8 asyncio.run(set_loc()) # Check memory: has visited (5, 5) mem_res = client.get(f"/api/players/{b1['id']}/memory?check_x=5&check_y=5") assert mem_res.status_code == 200 mem_data = mem_res.json() assert mem_data["has_visited_current"] is True assert mem_data["visited_count"] >= 1 # Check memory: has NOT visited (20, 20) mem_unvis = client.get(f"/api/players/{b1['id']}/memory?check_x=20&check_y=20").json() assert mem_unvis["has_visited_current"] is False # Check radar awareness radar_res = client.get(f"/api/players/{b1['id']}/radar") assert radar_res.status_code == 200 radar_data = radar_res.json() assert radar_data["bot_goal"] == "form_party" assert len(radar_data["targets"]) >= 1 assert radar_data["nearest_target"]["name"] == "BotBravo" assert radar_data["nearest_target"]["distance"] == 3 def test_unpartied_bot_seeks_party_autonomous_turn(): client = TestClient(app) b1 = client.post("/api/players", json={"name": "SeekerA", "color": "#00FFFF", "strength": 1}).json() b2 = client.post("/api/players", json={"name": "SeekerB", "color": "#FFFF00", "strength": 1}).json() async def setup_grid(): p1 = await game_engine.get_player(b1["id"]) p1.x, p1.y = 10, 10 p2 = await game_engine.get_player(b2["id"]) p2.x, p2.y = 10, 12 asyncio.run(setup_grid()) # Execute AI step for SeekerA (distance 2, should step down towards SeekerB) step_res = client.post(f"/api/players/{b1['id']}/ai-step") assert step_res.status_code == 200 data = step_res.json() assert data["bot_goal"] == "form_party" assert data["action_taken"] in ["moved", "formed_party"] def test_3bout_d20_battle_with_defeated_members_joining_winner(): client = TestClient(app) # Party 1: AlphaLeader + AlphaMember p1 = client.post("/api/players", json={"name": "AlphaLead", "color": "#111111", "strength": 5}).json() p2 = client.post("/api/players", json={"name": "AlphaWing", "color": "#222222", "strength": 5}).json() # Party 2: BetaLeader + BetaMember p3 = client.post("/api/players", json={"name": "BetaLead", "color": "#333333", "strength": 1}).json() p4 = client.post("/api/players", json={"name": "BetaWing", "color": "#444444", "strength": 1}).json() # Position them adjacent async def setup_combat_positions(): 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 b3 = await game_engine.get_player(p3["id"]) b3.x, b3.y = 11, 10 b4 = await game_engine.get_player(p4["id"]) b4.x, b4.y = 11, 11 asyncio.run(setup_combat_positions()) # Form Party 1 (Strength 10) client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"]], "leader_id": p1["id"], "name": "AlphaSquad", }) # Form Party 2 (Strength 2) client.post("/api/parties", json={ "member_ids": [p3["id"], p4["id"]], "leader_id": p3["id"], "name": "BetaSquad", }) # Fight battle between AlphaLead and BetaLead battle_res = client.post("/api/battles/fight", json={ "challenger_id": p1["id"], "defender_id": p3["id"], }) assert battle_res.status_code == 200 battle = battle_res.json() assert len(battle["bouts"]) == 3 assert battle["winner_party_name"] in ["AlphaSquad", "BetaSquad"] winner_leader = client.get(f"/api/players/{battle['winner_leader_id']}").json() assert winner_leader["score"] >= 2 killed_leader = client.get(f"/api/players/{battle['killed_leader_id']}").json() assert killed_leader["score"] == -1 assert killed_leader["party_id"] is None assert battle["new_party_size"] >= 2 def test_game_conclusion_and_scoreboard_rankings(): client = TestClient(app) p1 = client.post("/api/players", json={"name": "AlphaLeader", "color": "#FFD700", "strength": 5}).json() p2 = client.post("/api/players", json={"name": "BetaRival", "color": "#C0C0C0", "strength": 3}).json() p3 = client.post("/api/players", json={"name": "GammaHero", "color": "#CD7F32", "strength": 2}).json() p4 = client.post("/api/players", json={"name": "DeltaCadet", "color": "#4A5568", "strength": 1}).json() # Position all players contiguously within 1 unit async def setup_positions(): b1 = await game_engine.get_player(p1["id"]) b1.x, b1.y = 10, 10 b1.score = 6 # 1st place b2 = await game_engine.get_player(p2["id"]) b2.x, b2.y = 10, 11 b2.score = 4 # 2nd place b3 = await game_engine.get_player(p3["id"]) b3.x, b3.y = 10, 12 b3.score = 2 # 3rd place b4 = await game_engine.get_player(p4["id"]) b4.x, b4.y = 10, 13 b4.score = 0 # 4th place asyncio.run(setup_positions()) # Before joining all into 1 party, conclusion should be false conc_before = client.get("/api/game/conclusion").json() assert conc_before["concluded"] is False # Form 1 party that contains all 4 bots party_res = client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"], p3["id"], p4["id"]], "leader_id": p1["id"], "name": "UnitedLegion", }) assert party_res.status_code == 201 # Now there is just 1 party and all bots are in that party -> game concluded! conc_after = client.get("/api/game/conclusion").json() assert conc_after["concluded"] is True assert conc_after["winning_party_name"] == "UnitedLegion" assert conc_after["winning_leader_name"] == "AlphaLeader" assert conc_after["total_bots"] == 4 # Verify scoreboard rankings (highest score on top) rankings = conc_after["rankings"] assert len(rankings) == 4 assert rankings[0]["id"] == p1["id"] assert rankings[0]["score"] == 6 assert rankings[1]["id"] == p2["id"] assert rankings[1]["score"] == 4 assert rankings[2]["id"] == p3["id"] assert rankings[2]["score"] == 2 assert rankings[3]["id"] == p4["id"] assert rankings[3]["score"] == 0 # Board state also reflects conclusion board = client.get("/api/board").json() assert board["conclusion"]["concluded"] is True def test_solo_bot_refuses_weaker_leader_party_responds_with_battle(): """When a single bot is adjacent to a party with a lower strength leader, the bot refuses to join. The party responds by doing battle! When the party conquers the solo bot, the solo bot loses 1 point and is absorbed into the party. If all bots are now in the party, the game concludes! """ client = TestClient(app) # Party leader has strength 1, teammate has strength 10 (party total strength 11) p1 = client.post("/api/players", json={"name": "WeakLeader", "color": "#111111", "strength": 1}).json() p2 = client.post("/api/players", json={"name": "HeavyFollower", "color": "#222222", "strength": 10}).json() # Solo bot has strength 5 (strength 5 > leader's strength 1, so solo bot refuses to join!) p3 = client.post("/api/players", json={"name": "ProudSolo", "color": "#FF0000", "strength": 5}).json() async def setup_positions(): 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 b3 = await game_engine.get_player(p3["id"]) b3.x, b3.y = 11, 10 # adjacent to WeakLeader asyncio.run(setup_positions()) # Form party with WeakLeader and HeavyFollower client.post("/api/parties", json={ "member_ids": [p1["id"], p2["id"]], "leader_id": p1["id"], "name": "WeakSquad", }) # WeakLeader (or ProudSolo) takes their turn turn_info = client.get("/api/turn").json() acting_id = turn_info["current_player_id"] step_res = client.post(f"/api/players/{acting_id}/ai-step") assert step_res.status_code == 200 data = step_res.json() # Action taken must be "battled" because ProudSolo refused to join the weaker leader! assert data["action_taken"] == "battled" assert data["battle_result"] is not None battle = data["battle_result"] assert len(battle["bouts"]) == 3 # Check board state board = client.get("/api/board").json() # Both parties or party + solo bot resolved # If WeakSquad (total str 11) defeated ProudSolo (str 5): if battle["winner_party_name"] == "WeakSquad": # ProudSolo was conquered, received -1 point, and was absorbed into WeakSquad solo_after = client.get(f"/api/players/{p3['id']}").json() assert solo_after["score"] == -1 assert solo_after["party_id"] == battle["winner_party_id"] # WeakSquad leader received +2 points lead_after = client.get(f"/api/players/{p1['id']}").json() assert lead_after["score"] == 2 # All 3 bots on board are now in WeakSquad -> Game concluded! conc = client.get("/api/game/conclusion").json() assert conc["concluded"] is True assert conc["winning_party_name"] == "WeakSquad" assert conc["total_bots"] == 3