444 lines
19 KiB
Python
444 lines
19 KiB
Python
|
|
"""
|
||
|
|
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
|