2026-09-05 21:10:18 +00:00
|
|
|
import pytest
|
2026-09-05 23:50:27 +00:00
|
|
|
import asyncio
|
2026-09-05 21:10:18 +00:00
|
|
|
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")
|
2026-09-05 21:10:18 +00:00
|
|
|
yield
|
2026-09-05 23:50:27 +00:00
|
|
|
client.post("/api/reset")
|
2026-09-05 21:10:18 +00:00
|
|
|
|
|
|
|
|
|
2026-09-05 23:50:27 +00:00
|
|
|
def test_strength_based_leadership_negotiation():
|
2026-09-05 22:09:18 +00:00
|
|
|
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 22:09:18 +00:00
|
|
|
|
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 22:09:18 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2026-09-05 22:09:18 +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 22:09:18 +00:00
|
|
|
|
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():
|
2026-09-05 22:09:18 +00:00
|
|
|
b1 = await game_engine.get_player(p1["id"])
|
2026-09-05 23:50:27 +00:00
|
|
|
b1.x, b1.y = 10, 10
|
2026-09-05 22:09:18 +00:00
|
|
|
b2 = await game_engine.get_player(p2["id"])
|
2026-09-05 23:50:27 +00:00
|
|
|
b2.x, b2.y = 10, 11
|
2026-09-05 22:09:18 +00:00
|
|
|
b3 = await game_engine.get_player(p3["id"])
|
2026-09-05 23:50:27 +00:00
|
|
|
b3.x, b3.y = 11, 10
|
2026-09-05 22:09:18 +00:00
|
|
|
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"]],
|
2026-09-05 22:09:18 +00:00
|
|
|
"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={
|
2026-09-05 22:09:18 +00:00
|
|
|
"challenger_id": p1["id"],
|
2026-09-05 23:50:27 +00:00
|
|
|
"defender_id": p3["id"],
|
2026-09-05 22:09:18 +00:00
|
|
|
})
|
2026-09-05 23:50:27 +00:00
|
|
|
assert battle_res.status_code == 200
|
|
|
|
|
battle = battle_res.json()
|
2026-09-05 22:09:18 +00:00
|
|
|
|
|
|
|
|
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():
|
2026-09-05 21:10:18 +00:00
|
|
|
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():
|
2026-09-05 21:55:07 +00:00
|
|
|
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
|
|
|
|
|
|
2026-09-05 21:55:07 +00:00
|
|
|
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 21:55:07 +00:00
|
|
|
|
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 21:55:07 +00:00
|
|
|
|
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 21:55:07 +00:00
|
|
|
|
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!
|
|
|
|
|
"""
|
2026-09-05 21:10:18 +00:00
|
|
|
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():
|
2026-09-05 21:55:07 +00:00
|
|
|
b1 = await game_engine.get_player(p1["id"])
|
2026-09-05 23:50:27 +00:00
|
|
|
b1.x, b1.y = 10, 10
|
2026-09-05 21:55:07 +00:00
|
|
|
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 21:55:07 +00:00
|
|
|
|
2026-09-05 23:50:27 +00:00
|
|
|
# Form party with WeakLeader and HeavyFollower
|
2026-09-05 21:55:07 +00:00
|
|
|
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 21:55:07 +00:00
|
|
|
})
|
|
|
|
|
|
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
|