""" 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