botWebWars/backend/tests/test_api.py

251 lines
9.5 KiB
Python
Raw Normal View History

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_bot_location_memory_and_radar():
client = TestClient(app)
p1 = client.post("/api/players", json={"name": "MemoryBot", "color": "#00FFAA"}).json()
p2 = client.post("/api/players", json={"name": "NearbyBot", "color": "#FF00AA"}).json()
import asyncio
async def place_bots():
b1 = await game_engine.get_player(p1["id"])
b1.x, b1.y = 15, 15
b1.visited_locations = [{"x": 15, "y": 15}]
b2 = await game_engine.get_player(p2["id"])
b2.x, b2.y = 16, 16
b2.visited_locations = [{"x": 16, "y": 16}]
asyncio.run(place_bots())
# Check memory of p1
mem = client.get(f"/api/players/{p1['id']}/memory").json()
assert mem["player_id"] == p1["id"]
assert mem["current_x"] == 15
assert mem["current_y"] == 15
assert mem["visited_count"] == 1
assert mem["visited_history"][0] == {"x": 15, "y": 15}
# Check radar
radar = client.get(f"/api/players/{p1['id']}/radar").json()
assert radar["player_id"] == p1["id"]
assert len(radar["targets"]) == 1
assert radar["targets"][0]["id"] == p2["id"]
assert radar["targets"][0]["distance"] == 1
# Move p1
res_move = client.post(f"/api/players/{p1['id']}/move", json={"direction": "UP"})
assert res_move.status_code == 200
# Memory should now have 2 visited locations
mem2 = client.get(f"/api/players/{p1['id']}/memory").json()
assert mem2["visited_count"] == 2
assert mem2["visited_history"][-1] == {"x": 15, "y": 14}
2026-09-05 23:20:54 +00:00
def test_strength_based_leadership_negotiation():
"""Rule test:
- If a bot considers the other bot less than them (lower strength), they insist on being party
leader.
- A bot desires to join a bot that is equal or stronger.
"""
client = TestClient(app)
# Bot A (strength 10), Bot B (strength 2) placed adjacent
pA = client.post("/api/players", json={"name": "StrongBot", "color": "#111111", "strength": 10}).json()
pB = client.post("/api/players", json={"name": "WeakerBot", "color": "#222222", "strength": 2}).json()
import asyncio
async def place_adjacent():
bA = await game_engine.get_player(pA["id"])
bA.x, bA.y = 25, 25
bB = await game_engine.get_player(pB["id"])
bB.x, bB.y = 25, 26
asyncio.run(place_adjacent())
# Step AI for pA: see pB adjacent, negotiate party
ai_res = client.post(f"/api/players/{pA['id']}/ai-step")
assert ai_res.status_code == 200
data = ai_res.json()
assert data["action_taken"] == "formed_party"
assert data["formed_party"] is not None
# StrongBot insisted on being leader, WeakerBot joined stronger
formed = data["formed_party"]
assert formed["leader_id"] == pA["id"]
assert formed["leader_name"] == "StrongBot"
assert formed["total_strength"] == 12
botA_state = client.get(f"/api/players/{pA['id']}").json()
botB_state = client.get(f"/api/players/{pB['id']}").json()
assert botA_state["is_party_leader"] is True
assert botB_state["is_party_leader"] is False
assert botA_state["party_id"] == formed["id"]
assert botB_state["party_id"] == formed["id"]
def test_3bout_d20_battle_with_defeated_members_joining_winner():
client = TestClient(app)
# Party 1: Leader + 2 Followers (Total 3 bots, strength 3)
p1 = client.post("/api/players", json={"name": "SquadLeader1", "color": "#111111"}).json()
p2 = client.post("/api/players", json={"name": "WingmanA", "color": "#222222"}).json()
p3 = client.post("/api/players", json={"name": "WingmanB", "color": "#333333"}).json()
# Party 2: Leader + 1 Follower (Total 2 bots, strength 2)
p4 = client.post("/api/players", json={"name": "SquadLeader2", "color": "#444444"}).json()
p5 = client.post("/api/players", json={"name": "WingmanC", "color": "#555555"}).json()
import asyncio
async def setup_parties():
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
b4 = await game_engine.get_player(p4["id"])
b4.x, b4.y = 21, 20 # Adjacent to b1
b5 = await game_engine.get_player(p5["id"])
b5.x, b5.y = 22, 20
asyncio.run(setup_parties())
# Form Party 1
party1 = client.post("/api/parties", json={
"member_ids": [p1["id"], p2["id"], p3["id"]],
"leader_id": p1["id"],
"name": "Titans",
}).json()
# Form Party 2
party2 = client.post("/api/parties", json={
"member_ids": [p4["id"], p5["id"]],
"leader_id": p4["id"],
"name": "Phantoms",
}).json()
# Fight battle between the adjacent parties
res = client.post("/api/battles/fight", json={
"challenger_id": p1["id"],
"defender_id": p4["id"],
})
assert res.status_code == 200
battle = res.json()
# Verify 3 bouts were fought
assert len(battle["bouts"]) == 3
for bout in battle["bouts"]:
assert 1 <= bout["party1_roll"] <= 20
assert 1 <= bout["party2_roll"] <= 20
assert bout["party1_score"] == bout["party1_strength"] * bout["party1_roll"]
assert bout["party2_score"] == bout["party2_strength"] * bout["party2_roll"]
# Winner was crowned
winner_id = battle["winner_party_id"]
defeated_id = battle["defeated_party_id"]
assert winner_id in [party1["id"], party2["id"]]
assert defeated_id in [party1["id"], party2["id"]]
assert winner_id != defeated_id
# Defeated leader was killed and received -1 point
killed_leader_id = battle["killed_leader_id"]
dead_leader = client.get(f"/api/players/{killed_leader_id}").json()
assert dead_leader["score"] == -1
assert dead_leader["party_id"] is None
assert dead_leader["is_party_leader"] is False
# Winning leader received +2 points
winning_leader_id = battle["winner_leader_id"]
winning_leader = client.get(f"/api/players/{winning_leader_id}").json()
assert winning_leader["score"] == 2
# Winning original squad members received +1 point
# Defeated party members lost 0 points (score remains 0)
if winner_id == party1["id"]:
# Titans won: p2, p3 were in winning party -> score 1
# p5 was in defeated party -> score 0
assert client.get(f"/api/players/{p2['id']}").json()["score"] == 1
assert client.get(f"/api/players/{p3['id']}").json()["score"] == 1
assert client.get(f"/api/players/{p5['id']}").json()["score"] == 0
else:
# Phantoms won: p5 was in winning party -> score 1
# p2, p3 were in defeated party -> score 0
assert client.get(f"/api/players/{p5['id']}").json()["score"] == 1
assert client.get(f"/api/players/{p2['id']}").json()["score"] == 0
assert client.get(f"/api/players/{p3['id']}").json()["score"] == 0
# The remainder of the defeated party joined the winning party
winning_party = client.get(f"/api/parties/{winner_id}").json()
assert battle["new_party_size"] == len(winning_party["member_ids"])
# Absorbed bots are now members of winning_party
for absorbed_id in battle["absorbed_members"]:
absorbed_player = client.get(f"/api/players/{absorbed_id}").json()
assert absorbed_player["party_id"] == winner_id
assert absorbed_id in winning_party["member_ids"]
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()
2026-09-05 21:20:39 +00:00
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_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
res_follow = client.post(f"/api/players/{p2['id']}/move", json={"direction": "UP"})
assert res_follow.status_code == 403
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
2026-09-05 21:20:39 +00:00
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()
2026-09-05 21:20:39 +00:00
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"],
})
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"]