342 lines
15 KiB
Python
Raw Normal View History

2025-10-05 02:25:37 +02:00
"""
Multiplayer interaction tests - validates chat system, cursor movement,
building synchronization between multiple clients, and player connections
"""
import pytest
import asyncio
from tests.test_client import TestWebSocketClient, test_clients
class TestMultiplayerInteractions:
"""Test multiplayer functionality between multiple clients"""
@pytest.mark.asyncio
async def test_multiple_client_connections(self):
"""Test that multiple clients can connect simultaneously"""
async with test_clients("player1", "player2", "player3") as clients:
assert len(clients) == 3
# All clients should be connected
for client in clients:
assert client.connected == True
assert client.player_data is not None
assert client.get_money() == 100000 # Starting money
@pytest.mark.asyncio
async def test_player_join_notifications(self):
"""Test that clients receive notifications when players join"""
async with test_clients("first_player") as [client1]:
client1.clear_messages()
# Connect second player
async with test_clients("second_player") as [client2]:
# First client should receive join notification
message = await client1.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "player_joined"
assert message["nickname"] == "second_player"
assert "player_id" in message
@pytest.mark.asyncio
async def test_player_leave_notifications(self):
"""Test that clients receive notifications when players leave"""
async with test_clients("observer") as [observer]:
observer.clear_messages()
# Connect and disconnect another player
async with test_clients("temp_player") as [temp_client]:
# Clear join message
await observer.receive_message(timeout=1.0)
# Observer should receive leave notification
message = await observer.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "player_left"
assert message["nickname"] == "temp_player"
@pytest.mark.asyncio
async def test_chat_message_broadcasting(self):
"""Test that chat messages are broadcast to all clients"""
async with test_clients("sender", "receiver1", "receiver2") as clients:
sender, receiver1, receiver2 = clients
# Clear initial messages
for client in clients:
client.clear_messages()
# Send chat message
test_message = "Hello everyone!"
await sender.send_chat(test_message)
# Both receivers should get the chat message
for receiver in [receiver1, receiver2]:
message = await receiver.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "chat"
assert message["message"] == test_message
assert message["nickname"] == "sender"
assert "timestamp" in message
@pytest.mark.asyncio
async def test_chat_message_not_echoed_to_sender(self):
"""Test that chat messages are not echoed back to sender"""
async with test_clients("sender", "receiver") as [sender, receiver]:
# Clear initial messages
sender.clear_messages()
receiver.clear_messages()
# Send chat message
await sender.send_chat("Test message")
# Receiver should get message
receiver_msg = await receiver.receive_message(timeout=2.0)
assert receiver_msg["type"] == "chat"
# Sender should not receive their own message back
sender_msg = await sender.receive_message(timeout=1.0)
assert sender_msg is None or sender_msg["type"] != "chat"
@pytest.mark.asyncio
async def test_cursor_movement_synchronization(self):
"""Test that cursor movements are synchronized between clients"""
async with test_clients("mover", "observer") as [mover, observer]:
# Clear initial messages
observer.clear_messages()
# Send cursor movement
test_x, test_y = 10, 15
await mover.send_cursor_move(test_x, test_y)
# Observer should receive cursor movement
message = await observer.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "cursor_move"
assert message["x"] == test_x
assert message["y"] == test_y
assert "player_id" in message
@pytest.mark.asyncio
async def test_cursor_movement_not_echoed_to_sender(self):
"""Test that cursor movements are not echoed back to sender"""
async with test_clients("mover", "observer") as [mover, observer]:
# Clear initial messages
mover.clear_messages()
observer.clear_messages()
# Send cursor movement
await mover.send_cursor_move(5, 7)
# Observer should get movement
observer_msg = await observer.receive_message(timeout=2.0)
assert observer_msg["type"] == "cursor_move"
# Mover should not receive their own cursor movement back
mover_msg = await mover.receive_message(timeout=1.0)
assert mover_msg is None or mover_msg["type"] != "cursor_move"
@pytest.mark.asyncio
async def test_building_placement_synchronization(self):
"""Test that building placements are synchronized to all clients"""
async with test_clients("builder", "observer1", "observer2") as clients:
builder, observer1, observer2 = clients
# Clear initial messages
for client in clients:
client.clear_messages()
# Place a building
await builder.place_building("small_house", 5, 5)
# All clients should receive building placement notification
for observer in [observer1, observer2]:
message = await observer.receive_message(timeout=3.0)
assert message is not None
assert message["type"] == "building_placed"
assert message["building"]["type"] == "small_house"
assert message["building"]["x"] == 5
assert message["building"]["y"] == 5
assert message["building"]["owner_id"] == builder.player_data["player_id"]
@pytest.mark.asyncio
async def test_building_removal_synchronization(self):
"""Test that building removals are synchronized to all clients"""
async with test_clients("builder", "observer") as [builder, observer]:
# Place building first
await builder.place_building("small_house", 3, 3)
# Wait for placement to complete
placement_msg = await observer.receive_message(timeout=2.0)
assert placement_msg["type"] == "building_placed"
# Clear messages
observer.clear_messages()
# Remove the building
await builder.remove_building(3, 3)
# Observer should receive removal notification
message = await observer.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "building_removed"
assert message["x"] == 3
assert message["y"] == 3
@pytest.mark.asyncio
async def test_building_edit_synchronization(self):
"""Test that building name edits are synchronized to all clients"""
async with test_clients("builder", "observer") as [builder, observer]:
# Place building first
await builder.place_building("small_house", 7, 7)
# Wait for placement
placement_msg = await observer.receive_message(timeout=2.0)
assert placement_msg["type"] == "building_placed"
# Clear messages
observer.clear_messages()
# Edit building name
new_name = "My House"
await builder.edit_building(7, 7, new_name)
# Observer should receive edit notification
message = await observer.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "building_updated"
assert message["x"] == 7
assert message["y"] == 7
assert message["name"] == new_name
@pytest.mark.asyncio
async def test_building_ownership_permissions(self):
"""Test that only building owners can edit/delete buildings"""
async with test_clients("owner", "other_player") as [owner, other_player]:
# Owner places building
await owner.place_building("small_house", 10, 10)
# Wait for placement
await asyncio.sleep(0.5)
# Other player tries to remove owner's building
await other_player.remove_building(10, 10)
# Should receive error message
message = await other_player.receive_message(timeout=2.0)
assert message is not None
assert message["type"] == "error"
assert "don't own" in message["message"].lower()
@pytest.mark.asyncio
async def test_multiple_simultaneous_actions(self):
"""Test handling multiple simultaneous client actions"""
async with test_clients("player1", "player2", "player3") as clients:
# All players perform actions simultaneously
tasks = []
# Player 1 places buildings
tasks.append(clients[0].place_building("small_house", 0, 0))
tasks.append(clients[0].place_building("road", 1, 0))
# Player 2 sends chat and moves cursor
tasks.append(clients[1].send_chat("Building my city!"))
tasks.append(clients[1].send_cursor_move(15, 20))
# Player 3 places building
tasks.append(clients[2].place_building("small_shop", 5, 5))
# Execute all actions simultaneously
await asyncio.gather(*tasks)
# Give time for all messages to propagate
await asyncio.sleep(1.0)
# Collect all messages from all clients
all_messages = []
for client in clients:
messages = await client.receive_messages_for(2.0)
all_messages.extend(messages)
# Should have received various message types
message_types = [msg["type"] for msg in all_messages]
assert "building_placed" in message_types
assert "chat" in message_types
assert "cursor_move" in message_types
@pytest.mark.asyncio
async def test_player_color_uniqueness(self):
"""Test that each player gets a unique color"""
async with test_clients("red", "green", "blue") as clients:
colors = []
for client in clients:
color = client.player_data["color"]
assert color.startswith("#") # Hex color format
assert len(color) == 7 # #RRGGBB format
colors.append(color)
# All colors should be different (very likely with random generation)
assert len(set(colors)) == len(colors)
@pytest.mark.asyncio
async def test_reconnection_with_same_nickname(self):
"""Test that reconnecting with same nickname restores player data"""
# Connect first time
async with test_clients("reconnect_test") as [client1]:
original_player_id = client1.player_data["player_id"]
original_color = client1.player_data["color"]
# Place a building to modify game state
await client1.place_building("small_house", 0, 0)
await asyncio.sleep(0.5) # Wait for placement
# Reconnect with same nickname
async with test_clients("reconnect_test") as [client2]:
# Should have same player ID and color
assert client2.player_data["player_id"] == original_player_id
assert client2.player_data["color"] == original_color
# Money should be reduced by building cost
assert client2.get_money() == 100000 - 5000 # Started with 100k, spent 5k on house
class TestMultiplayerStressTests:
"""Stress tests for multiplayer functionality"""
@pytest.mark.asyncio
async def test_rapid_chat_messages(self):
"""Test handling of rapid chat message bursts"""
async with test_clients("chatter", "listener") as [chatter, listener]:
listener.clear_messages()
# Send many messages quickly
messages = [f"Message {i}" for i in range(10)]
tasks = [chatter.send_chat(msg) for msg in messages]
await asyncio.gather(*tasks)
# Collect received messages
received = await listener.receive_messages_for(3.0)
chat_messages = [msg for msg in received if msg["type"] == "chat"]
# Should receive all messages (though order might vary)
assert len(chat_messages) >= 8 # Allow for some timing issues
@pytest.mark.asyncio
async def test_rapid_cursor_movements(self):
"""Test handling of rapid cursor movement updates"""
async with test_clients("mover", "observer") as [mover, observer]:
observer.clear_messages()
# Send many cursor movements quickly
positions = [(i, i*2) for i in range(20)]
tasks = [mover.send_cursor_move(x, y) for x, y in positions]
await asyncio.gather(*tasks)
# Collect received movements
received = await observer.receive_messages_for(2.0)
cursor_messages = [msg for msg in received if msg["type"] == "cursor_move"]
# Should receive cursor updates (may be throttled by server)
assert len(cursor_messages) > 0
# Last position should be among the received messages
last_positions = [(msg["x"], msg["y"]) for msg in cursor_messages[-3:]]
assert (19, 38) in last_positions # Last position from our list