botWebWars/backend/tests/test_api.py

295 lines
11 KiB
Python
Raw Normal View History

import pytest
2026-09-05 23:50:27 +00:00
import asyncio
from fastapi.testclient import TestClient
from app.main import app
from app.game import game_engine
@pytest.fixture(autouse=True)
2026-09-05 23:50:27 +00:00
def reset_game_state():
client = TestClient(app)
client.post("/api/reset")
yield
2026-09-05 23:50:27 +00:00
client.post("/api/reset")
2026-09-05 23:50:27 +00:00
def test_strength_based_leadership_negotiation():
client = TestClient(app)
2026-09-05 23:50:27 +00:00
# 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
2026-09-05 23:50:27 +00:00
# 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
2026-09-05 23:20:54 +00:00
2026-09-05 23:50:27 +00:00
def test_bot_location_memory_and_radar():
2026-09-05 23:20:54 +00:00
client = TestClient(app)
2026-09-05 23:50:27 +00:00
# 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()
2026-09-05 23:20:54 +00:00
2026-09-05 23:50:27 +00:00
assert data["bot_goal"] == "form_party"
assert data["action_taken"] in ["moved", "formed_party"]
2026-09-05 23:20:54 +00:00
def test_3bout_d20_battle_with_defeated_members_joining_winner():
client = TestClient(app)
2026-09-05 23:50:27 +00:00
# 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()
2026-09-05 23:50:27 +00:00
# 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"])
2026-09-05 23:50:27 +00:00
b1.x, b1.y = 10, 10
b2 = await game_engine.get_player(p2["id"])
2026-09-05 23:50:27 +00:00
b2.x, b2.y = 10, 11
b3 = await game_engine.get_player(p3["id"])
2026-09-05 23:50:27 +00:00
b3.x, b3.y = 11, 10
b4 = await game_engine.get_player(p4["id"])
2026-09-05 23:50:27 +00:00
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"],
2026-09-05 23:50:27 +00:00
"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"],
2026-09-05 23:50:27 +00:00
"defender_id": p3["id"],
})
2026-09-05 23:50:27 +00:00
assert battle_res.status_code == 200
battle = battle_res.json()
assert len(battle["bouts"]) == 3
2026-09-05 23:50:27 +00:00
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)
2026-09-05 21:20:39 +00:00
2026-09-05 23:50:27 +00:00
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
2026-09-05 23:50:27 +00:00
b1.score = 6 # 1st place
b2 = await game_engine.get_player(p2["id"])
b2.x, b2.y = 10, 11
2026-09-05 23:50:27 +00:00
b2.score = 4 # 2nd place
2026-09-05 23:50:27 +00:00
b3 = await game_engine.get_player(p3["id"])
b3.x, b3.y = 10, 12
b3.score = 2 # 3rd place
2026-09-05 23:50:27 +00:00
b4 = await game_engine.get_player(p4["id"])
b4.x, b4.y = 10, 13
b4.score = 0 # 4th place
asyncio.run(setup_positions())
2026-09-05 23:50:27 +00:00
# Before joining all into 1 party, conclusion should be false
conc_before = client.get("/api/game/conclusion").json()
assert conc_before["concluded"] is False
2026-09-05 21:20:39 +00:00
2026-09-05 23:50:27 +00:00
# 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)
2026-09-05 21:20:39 +00:00
2026-09-05 23:50:27 +00:00
# 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"])
2026-09-05 23:50:27 +00:00
b1.x, b1.y = 10, 10
b2 = await game_engine.get_player(p2["id"])
2026-09-05 23:50:27 +00:00
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())
2026-09-05 23:50:27 +00:00
# Form party with WeakLeader and HeavyFollower
client.post("/api/parties", json={
"member_ids": [p1["id"], p2["id"]],
"leader_id": p1["id"],
2026-09-05 23:50:27 +00:00
"name": "WeakSquad",
})
2026-09-05 23:50:27 +00:00
# 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