"""
Integration tests - End-to-end tests that simulate complete game scenarios
with multiple players, testing all systems working together
"""
import pytest
import asyncio
from tests.test_client import test_clients, TestGameServer
class TestCompleteGameScenarios:
"""End-to-end integration tests simulating real game scenarios"""
@pytest.mark.asyncio
async def test_two_player_city_building_session(self):
"""Test a complete two-player game session from start to finish"""
async with test_clients("alice", "bob") as [alice, bob]:
# Verify both players start with correct initial state
assert alice.get_money() == 100000
assert bob.get_money() == 100000
assert alice.get_population() == 0
assert bob.get_population() == 0
# Alice builds a residential area
await alice.place_building("small_house", 0, 0)
await alice.place_building("small_house", 1, 0)
await alice.place_building("road", 0, 1)
await alice.place_building("road", 1, 1)
# Bob builds a commercial area
await bob.place_building("small_house", 10, 10) # For population first
await asyncio.sleep(0.2) # Let buildings place
await bob.place_building("small_shop", 11, 10)
await bob.place_building("road", 10, 11)
await bob.place_building("road", 11, 11)
# Give time for all buildings to be placed and synchronized
await asyncio.sleep(1.0)
# Collect all messages from both players
alice_messages = await alice.receive_messages_for(2.0)
bob_messages = await bob.receive_messages_for(2.0)
# Both players should see all building placements
alice_building_msgs = [m for m in alice_messages if m["type"] == "building_placed"]
bob_building_msgs = [m for m in bob_messages if m["type"] == "building_placed"]
# Each player should see the other's buildings
alice_saw_bob_buildings = any(
msg["building"]["owner_id"] == bob.player_data["player_id"]
for msg in alice_building_msgs
)
bob_saw_alice_buildings = any(
msg["building"]["owner_id"] == alice.player_data["player_id"]
for msg in bob_building_msgs
)
assert alice_saw_bob_buildings
assert bob_saw_alice_buildings
@pytest.mark.asyncio
async def test_collaborative_city_with_chat(self):
"""Test players collaborating on a city with chat communication"""
async with test_clients("architect", "builder") as [architect, builder]:
# Clear initial messages
architect.clear_messages()
builder.clear_messages()
# Architect announces the plan
await architect.send_chat("Let's build a connected city!")
# Builder responds
await builder.send_chat("Great idea! I'll help with the roads.")
# Collaborative building - architect places houses
tasks = [
architect.place_building("small_house", 5, 5),
architect.place_building("small_house", 6, 5),
builder.place_building("road", 5, 6),
builder.place_building("road", 6, 6),
builder.place_building("road", 7, 6)
]
await asyncio.gather(*tasks)
# Give time for messages to propagate
await asyncio.sleep(1.0)
# Both should have received chat messages
architect_messages = await architect.receive_messages_for(2.0)
builder_messages = await builder.receive_messages_for(2.0)
# Find chat messages
architect_chats = [m for m in architect_messages if m["type"] == "chat"]
builder_chats = [m for m in builder_messages if m["type"] == "chat"]
# Each player should see the other's chat messages
architect_saw_builder_chat = any(
m["nickname"] == "builder" and "roads" in m["message"]
for m in architect_chats
)
builder_saw_architect_chat = any(
m["nickname"] == "architect" and "connected city" in m["message"]
for m in builder_chats
)
assert architect_saw_builder_chat
assert builder_saw_architect_chat
@pytest.mark.asyncio
async def test_economy_progression_scenario(self):
"""Test a complete economy progression from houses to advanced buildings"""
async with test_clients("tycoon") as [player]:
initial_money = player.get_money()
# Phase 1: Build basic infrastructure
await player.place_building("small_house", 0, 0)
await player.place_building("small_house", 1, 0)
await player.place_building("road", 0, 1)
await player.place_building("road", 1, 1)
# Wait for economy tick to give population
await asyncio.sleep(2.0)
# Collect economy updates
messages = await player.receive_messages_for(1.0)
stats_updates = [m for m in messages if m["type"] == "player_stats_update"]
if stats_updates:
# Should have population from houses
latest_stats = stats_updates[-1]["player"]
assert latest_stats["population"] > 0
# Money should be less due to house costs but we might have some income
current_money = latest_stats["money"]
house_costs = 5000 * 2 # Two houses
road_costs = 500 * 2 # Two roads
total_costs = house_costs + road_costs
# Money should be reduced by at least the building costs
assert current_money <= initial_money - total_costs
# Phase 2: Build commercial buildings
await asyncio.sleep(0.5) # Brief pause
await player.place_building("small_shop", 2, 0)
# Phase 3: Try to build advanced buildings (if we have population)
await asyncio.sleep(1.0)
messages = await player.receive_messages_for(1.0)
# The test validates the progression happens without errors
# Exact money values are hard to predict due to timing of economy ticks
@pytest.mark.asyncio
async def test_competitive_building_scenario(self):
"""Test competitive scenario where players build in nearby areas"""
async with test_clients("red_team", "blue_team") as [red, blue]:
# Both teams start building near each other
red_tasks = [
red.place_building("small_house", 0, 0),
red.place_building("small_house", 1, 0),
red.place_building("road", 0, 1),
red.place_building("road", 1, 1)
]
blue_tasks = [
blue.place_building("small_house", 3, 0),
blue.place_building("small_house", 4, 0),
blue.place_building("road", 3, 1),
blue.place_building("road", 4, 1)
]
# Execute both teams' actions simultaneously
await asyncio.gather(*red_tasks, *blue_tasks)
# Both teams try to build a connecting road in the middle
await red.place_building("road", 2, 1)
await asyncio.sleep(0.1) # Small delay
await blue.place_building("road", 2, 0) # Different position
# Give time for all actions to complete
await asyncio.sleep(1.0)
# Collect results
red_messages = await red.receive_messages_for(2.0)
blue_messages = await blue.receive_messages_for(2.0)
# Count successful building placements
red_buildings = [m for m in red_messages if m["type"] == "building_placed"]
blue_buildings = [m for m in blue_messages if m["type"] == "building_placed"]
# Both teams should see all building placements from both sides
total_building_messages = len(red_buildings) + len(blue_buildings)
assert total_building_messages > 8 # Should see buildings from both players
@pytest.mark.asyncio
async def test_player_reconnection_scenario(self):
"""Test player disconnection and reconnection maintaining state"""
# First session - player builds initial city
async with test_clients("persistent_player") as [player1]:
await player1.place_building("small_house", 5, 5)
await player1.place_building("road", 5, 6)
await player1.send_chat("Building my first house!")
# Wait for actions to complete
await asyncio.sleep(1.0)
money_after_building = player1.get_money()
# Player disconnects and reconnects
async with test_clients("persistent_player") as [player2]:
# Player should have same reduced money from previous session
assert player2.get_money() <= money_after_building
# Should be able to continue building
await player2.place_building("small_shop", 6, 5)
# Should receive confirmation
message = await player2.receive_message(timeout=2.0)
if message: # Might fail if no population yet, that's expected
assert message["type"] in ["building_placed", "error"]
@pytest.mark.asyncio
async def test_large_scale_multiplayer_scenario(self):
"""Test scenario with multiple players building simultaneously"""
async with test_clients("player1", "player2", "player3", "player4") as players:
# Each player builds in their own quadrant
tasks = []
# Player 1: Top-left quadrant (residential)
tasks.extend([
players[0].place_building("small_house", 0, 0),
players[0].place_building("small_house", 1, 0),
players[0].place_building("road", 0, 1)
])
# Player 2: Top-right quadrant (commercial)
tasks.extend([
players[1].place_building("small_house", 10, 0),
players[1].place_building("small_shop", 11, 0),
players[1].place_building("road", 10, 1)
])
# Player 3: Bottom-left quadrant (industrial)
tasks.extend([
players[2].place_building("small_factory", 0, 10),
players[2].place_building("road", 0, 11),
players[2].place_building("road", 1, 11)
])
# Player 4: Infrastructure and chat
tasks.extend([
players[3].place_building("road", 5, 5),
players[3].place_building("road", 5, 6),
players[3].send_chat("Central hub complete!")
])
# Execute all actions simultaneously
await asyncio.gather(*tasks, return_exceptions=True)
# Give time for all messages to propagate
await asyncio.sleep(2.0)
# Each player should have received updates from others
all_messages = []
for player in players:
messages = await player.receive_messages_for(1.0)
all_messages.extend(messages)
# Should have various message types
message_types = [msg["type"] for msg in all_messages]
assert "building_placed" in message_types
assert "chat" in message_types
# Should have messages from multiple players
unique_owners = set()
for msg in all_messages:
if msg["type"] == "building_placed":
unique_owners.add(msg["building"]["owner_id"])
elif msg["type"] == "chat":
# Chat from player 4
assert msg["nickname"] == "player4"
assert len(unique_owners) > 1 # Multiple players placed buildings
@pytest.mark.asyncio
async def test_error_handling_in_multiplayer(self):
"""Test error handling when multiple players make conflicting actions"""
async with test_clients("player_a", "player_b") as [player_a, player_b]:
# Both players try to place building at same location
await asyncio.gather(
player_a.place_building("small_house", 0, 0),
player_b.place_building("road", 0, 0),
return_exceptions=True
)
# Give time for responses
await asyncio.sleep(1.0)
# Collect messages
a_messages = await player_a.receive_messages_for(1.0)
b_messages = await player_b.receive_messages_for(1.0)
# One should succeed, one should get error
a_success = any(m["type"] == "building_placed" for m in a_messages)
a_error = any(m["type"] == "error" for m in a_messages)
b_success = any(m["type"] == "building_placed" for m in b_messages)
b_error = any(m["type"] == "error" for m in b_messages)
# Exactly one should succeed and one should get error
assert (a_success and b_error) or (b_success and a_error)
@pytest.mark.asyncio
async def test_road_network_economy_integration(self):
"""Test integration of road networks with economy system"""
async with test_clients("network_builder") as [player]:
# Build isolated shop (no road bonus)
await player.place_building("small_house", 0, 0) # For population
await asyncio.sleep(0.5)
await player.place_building("small_shop", 5, 5)
# Wait for economy tick
await asyncio.sleep(2.0)
# Get baseline income
messages = await player.receive_messages_for(1.0)
baseline_money = player.get_money()
# Build road network around shop
await player.place_building("road", 5, 4) # Adjacent to shop
await player.place_building("road", 5, 3) # Extend network
await player.place_building("road", 6, 3) # Extend network
await player.place_building("road", 7, 3) # Large network
# Wait for economy tick with road bonuses
await asyncio.sleep(2.5)
# Should receive economy updates
messages = await player.receive_messages_for(1.0)
stats_updates = [m for m in messages if m["type"] == "player_stats_update"]
if stats_updates:
# With larger road network, income should be higher
final_money = stats_updates[-1]["player"]["money"]
# Due to timing, this is hard to test precisely, but we can verify
# the system is working and no errors occurred
assert final_money is not None
class TestStressAndPerformance:
"""Stress tests to validate system performance and stability"""
@pytest.mark.asyncio
async def test_rapid_building_placement(self):
"""Test system handling rapid building placements"""
async with test_clients("speed_builder") as [player]:
# Rapidly place many buildings
tasks = []
for i in range(20):
x, y = i % 5, i // 5
if i % 2 == 0:
tasks.append(player.place_building("road", x, y))
else:
tasks.append(player.place_building("small_house", x, y))
# Execute all at once (some may fail due to insufficient funds)
results = await asyncio.gather(*tasks, return_exceptions=True)
# Give time for all responses
await asyncio.sleep(2.0)
# Collect all messages
messages = await player.receive_messages_for(3.0)
# Should have received various responses without crashing
assert len(messages) > 0
# Mix of success and error messages is expected
successes = [m for m in messages if m["type"] == "building_placed"]
errors = [m for m in messages if m["type"] == "error"]
# Should have some successes (at least early ones before money runs out)
assert len(successes) > 0
@pytest.mark.asyncio
async def test_concurrent_chat_flood(self):
"""Test handling of many concurrent chat messages"""
async with test_clients("chatter1", "chatter2") as [chatter1, chatter2]:
# Clear initial messages
chatter1.clear_messages()
chatter2.clear_messages()
# Both players send many messages quickly
tasks = []
for i in range(15):
tasks.append(chatter1.send_chat(f"Message from chatter1: {i}"))
tasks.append(chatter2.send_chat(f"Message from chatter2: {i}"))
await asyncio.gather(*tasks, return_exceptions=True)
# Collect messages
await asyncio.sleep(2.0)
messages1 = await chatter1.receive_messages_for(3.0)
messages2 = await chatter2.receive_messages_for(3.0)
# Both should have received chat messages
chat1 = [m for m in messages1 if m["type"] == "chat"]
chat2 = [m for m in messages2 if m["type"] == "chat"]
# Should have received substantial number of messages
assert len(chat1) > 10
assert len(chat2) > 10
@pytest.mark.asyncio
async def test_mixed_action_stress_test(self):
"""Test system under mixed load of all action types"""
async with test_clients("stress_tester") as [player]:
# Clear messages
player.clear_messages()
# Mix of different actions
tasks = []
for i in range(10):
# Building placement
tasks.append(player.place_building("road", i, 0))
# Chat messages
tasks.append(player.send_chat(f"Building road {i}"))
# Cursor movements
tasks.append(player.send_cursor_move(i * 2, i * 3))
# Execute all actions
await asyncio.gather(*tasks, return_exceptions=True)
# System should handle this without crashing
await asyncio.sleep(2.0)
# Should receive various message types
messages = await player.receive_messages_for(3.0)
message_types = set(msg["type"] for msg in messages)
# Should have received multiple types of responses
assert len(message_types) >= 2 # At least building and chat responses
assert "building_placed" in message_types or "error" in message_types