462 lines
19 KiB
Python
Raw Normal View History

2025-10-05 02:25:37 +02:00
"""
Game state and persistence tests - validates building placement/removal,
player creation, database save/load operations, and game state integrity
"""
import pytest
import asyncio
import tempfile
import os
from server.game_state import GameState
from server.economy import EconomyEngine
from server.database import Database
from server.models import Player, Building, BuildingType
from tests.test_client import test_clients
class TestGameStateManagement:
"""Test core game state functionality"""
@pytest.fixture
def game_setup(self):
"""Setup fresh game state for testing"""
game_state = GameState()
return game_state
def test_player_creation(self, game_setup):
"""Test player creation and retrieval"""
game_state = game_setup
# Create new player
player = game_state.get_or_create_player("test_user", "test_123")
assert player.nickname == "test_user"
assert player.player_id == "test_123"
assert player.money == 100000 # Starting money
assert player.population == 0
assert player.is_online == True
assert player.color.startswith("#")
# Retrieve existing player
same_player = game_state.get_or_create_player("test_user", "test_123")
assert same_player == player
assert same_player.player_id == "test_123"
def test_building_placement_validation(self, game_setup):
"""Test building placement validation rules"""
game_state = game_setup
player = game_state.get_or_create_player("builder", "builder_123")
# Valid building placement
result = game_state.place_building("builder_123", "small_house", 0, 0)
assert result["success"] == True
assert (0, 0) in game_state.buildings
# Cannot place on occupied tile
result = game_state.place_building("builder_123", "road", 0, 0)
assert result["success"] == False
assert "already occupied" in result["error"]
# Cannot place with insufficient funds
player.money = 100 # Very low money
result = game_state.place_building("builder_123", "power_plant", 1, 1)
assert result["success"] == False
assert "not enough money" in result["error"].lower()
def test_building_removal_validation(self, game_setup):
"""Test building removal validation rules"""
game_state = game_setup
owner = game_state.get_or_create_player("owner", "owner_123")
other = game_state.get_or_create_player("other", "other_123")
# Place building
game_state.place_building("owner_123", "small_house", 5, 5)
# Owner can remove their building
result = game_state.remove_building("owner_123", 5, 5)
assert result["success"] == True
assert (5, 5) not in game_state.buildings
# Place another building
game_state.place_building("owner_123", "small_house", 6, 6)
# Other player cannot remove owner's building
result = game_state.remove_building("other_123", 6, 6)
assert result["success"] == False
assert "don't own" in result["error"].lower()
# Cannot remove non-existent building
result = game_state.remove_building("owner_123", 10, 10)
assert result["success"] == False
assert "no building" in result["error"].lower()
def test_building_edit_validation(self, game_setup):
"""Test building name editing validation"""
game_state = game_setup
owner = game_state.get_or_create_player("owner", "owner_123")
other = game_state.get_or_create_player("other", "other_123")
# Place building
game_state.place_building("owner_123", "small_house", 7, 7)
# Owner can edit their building
result = game_state.edit_building_name("owner_123", 7, 7, "My Home")
assert result["success"] == True
assert game_state.buildings[(7, 7)].name == "My Home"
# Other player cannot edit owner's building
result = game_state.edit_building_name("other_123", 7, 7, "Stolen House")
assert result["success"] == False
assert "don't own" in result["error"].lower()
# Name should remain unchanged
assert game_state.buildings[(7, 7)].name == "My Home"
def test_road_network_connectivity(self, game_setup):
"""Test road network connectivity calculations"""
game_state = game_setup
player = game_state.get_or_create_player("builder", "builder_123")
# Place disconnected roads
game_state.place_building("builder_123", "road", 0, 0)
game_state.place_building("builder_123", "road", 5, 5)
# Should have 2 separate zones
assert len(game_state.connected_zones) == 2
assert {(0, 0)} in game_state.connected_zones
assert {(5, 5)} in game_state.connected_zones
# Connect the roads
game_state.place_building("builder_123", "road", 1, 0)
game_state.place_building("builder_123", "road", 2, 0)
game_state.place_building("builder_123", "road", 3, 0)
game_state.place_building("builder_123", "road", 4, 0)
game_state.place_building("builder_123", "road", 4, 1)
game_state.place_building("builder_123", "road", 4, 2)
game_state.place_building("builder_123", "road", 4, 3)
game_state.place_building("builder_123", "road", 4, 4)
game_state.place_building("builder_123", "road", 4, 5)
game_state.place_building("builder_123", "road", 5, 4)
# Should now be one connected zone
assert len(game_state.connected_zones) == 1
assert len(game_state.connected_zones[0]) == 10 # All roads connected
def test_zone_size_calculation(self, game_setup):
"""Test building zone size calculation for economy bonuses"""
game_state = game_setup
player = game_state.get_or_create_player("builder", "builder_123")
# Place building with no adjacent roads
game_state.place_building("builder_123", "small_shop", 10, 10)
zone_size = game_state.get_building_zone_size(10, 10)
assert zone_size == 0
# Place roads near the building
game_state.place_building("builder_123", "road", 10, 11) # Adjacent
game_state.place_building("builder_123", "road", 10, 12) # Connected
game_state.place_building("builder_123", "road", 11, 12) # Extend network
zone_size = game_state.get_building_zone_size(10, 10)
assert zone_size == 3 # Size of connected road network
def test_power_plant_requirement_checking(self, game_setup):
"""Test power plant requirement validation"""
game_state = game_setup
player = game_state.get_or_create_player("builder", "builder_123")
player.money = 200000 # Enough for expensive buildings
# Cannot place large building without power plant
result = game_state.place_building("builder_123", "large_factory", 0, 0)
assert result["success"] == False
assert "power plant" in result["error"].lower()
# Place power plant first
result = game_state.place_building("builder_123", "power_plant", 5, 5)
assert result["success"] == True
# Now large building should work
result = game_state.place_building("builder_123", "large_factory", 0, 0)
assert result["success"] == True
def test_player_building_retrieval(self, game_setup):
"""Test retrieving all buildings owned by a player"""
game_state = game_setup
player1 = game_state.get_or_create_player("player1", "p1")
player2 = game_state.get_or_create_player("player2", "p2")
# Each player places buildings
game_state.place_building("p1", "small_house", 0, 0)
game_state.place_building("p1", "road", 1, 0)
game_state.place_building("p2", "small_shop", 2, 2)
# Check player buildings
p1_buildings = game_state.get_player_buildings("p1")
p2_buildings = game_state.get_player_buildings("p2")
assert len(p1_buildings) == 2
assert len(p2_buildings) == 1
# Verify building ownership
building_types = [b.building_type.value for b in p1_buildings]
assert "small_house" in building_types
assert "road" in building_types
assert p2_buildings[0].building_type.value == "small_shop"
def test_game_state_serialization(self, game_setup):
"""Test game state serialization for network transmission"""
game_state = game_setup
# Add some players and buildings
player1 = game_state.get_or_create_player("user1", "u1")
player2 = game_state.get_or_create_player("user2", "u2")
game_state.place_building("u1", "small_house", 0, 0)
game_state.place_building("u2", "road", 1, 1)
# Get serialized state
state = game_state.get_state()
# Verify structure
assert "players" in state
assert "buildings" in state
assert len(state["players"]) == 2
assert len(state["buildings"]) == 2
# Verify player data
assert "u1" in state["players"]
assert "u2" in state["players"]
assert state["players"]["u1"]["nickname"] == "user1"
assert state["players"]["u1"]["money"] < 100000 # Money spent on building
# Verify building data
assert "0,0" in state["buildings"]
assert "1,1" in state["buildings"]
assert state["buildings"]["0,0"]["type"] == "small_house"
assert state["buildings"]["0,0"]["owner_id"] == "u1"
class TestDatabasePersistence:
"""Test database save/load functionality"""
@pytest.fixture
def temp_database(self):
"""Create temporary database for testing"""
# Create temporary database file
temp_fd, temp_path = tempfile.mkstemp(suffix=".db")
os.close(temp_fd)
# Create database instance
database = Database()
# Override the default path for testing
database.db_path = temp_path
database.init_db()
yield database
# Cleanup
if os.path.exists(temp_path):
os.unlink(temp_path)
def test_database_initialization(self, temp_database):
"""Test database schema creation"""
db = temp_database
# Database should be initialized without errors
# Tables should exist (this is tested implicitly by other operations)
assert os.path.exists(db.db_path)
def test_save_and_load_game_state(self, temp_database):
"""Test saving and loading complete game state"""
db = temp_database
# Create game state with data
game_state = GameState()
player1 = game_state.get_or_create_player("test_user", "test_123")
player2 = game_state.get_or_create_player("other_user", "other_456")
# Modify player data
player1.money = 75000
player1.population = 50
player2.money = 120000
# Add buildings
game_state.place_building("test_123", "small_house", 5, 5)
game_state.place_building("other_456", "road", 10, 10)
game_state.buildings[(5, 5)].name = "Custom House"
# Save to database
db.save_game_state(game_state)
# Create new game state and load
new_game_state = GameState()
loaded_data = db.load_game_state()
new_game_state.load_state(loaded_data)
# Verify players were loaded correctly
assert len(new_game_state.players) == 2
assert "test_123" in new_game_state.players
assert "other_456" in new_game_state.players
loaded_player1 = new_game_state.players["test_123"]
assert loaded_player1.nickname == "test_user"
assert loaded_player1.money == 75000
assert loaded_player1.population == 50
assert loaded_player1.is_online == False # Players start offline when loaded
# Verify buildings were loaded correctly
assert len(new_game_state.buildings) == 2
assert (5, 5) in new_game_state.buildings
assert (10, 10) in new_game_state.buildings
house = new_game_state.buildings[(5, 5)]
assert house.building_type == BuildingType.SMALL_HOUSE
assert house.owner_id == "test_123"
assert house.name == "Custom House"
road = new_game_state.buildings[(10, 10)]
assert road.building_type == BuildingType.ROAD
assert road.owner_id == "other_456"
def test_empty_database_load(self, temp_database):
"""Test loading from empty database"""
db = temp_database
# Load from empty database
loaded_data = db.load_game_state()
# Should return empty structure
assert loaded_data == {"players": [], "buildings": []}
# Loading into game state should work without errors
game_state = GameState()
game_state.load_state(loaded_data)
assert len(game_state.players) == 0
assert len(game_state.buildings) == 0
def test_database_persistence_across_saves(self, temp_database):
"""Test that database persists data across multiple save operations"""
db = temp_database
game_state = GameState()
# Save initial state
player = game_state.get_or_create_player("persistent_user", "persist_123")
db.save_game_state(game_state)
# Modify and save again
game_state.place_building("persist_123", "small_house", 0, 0)
db.save_game_state(game_state)
# Add more data and save again
game_state.place_building("persist_123", "road", 1, 1)
player.money = 50000
db.save_game_state(game_state)
# Load fresh game state
new_game_state = GameState()
loaded_data = db.load_game_state()
new_game_state.load_state(loaded_data)
# Should have all the data from the last save
assert len(new_game_state.players) == 1
assert len(new_game_state.buildings) == 2
assert new_game_state.players["persist_123"].money == 50000
class TestGameStateIntegration:
"""Integration tests for game state with WebSocket clients"""
@pytest.mark.asyncio
async def test_building_placement_through_websocket(self):
"""Test building placement state changes through WebSocket"""
async with test_clients("state_test") as [client]:
initial_buildings = len(client.game_state["buildings"])
# Place building
await client.place_building("small_house", 3, 3)
# Wait for placement confirmation
message = await client.receive_message(timeout=2.0)
assert message["type"] == "building_placed"
# Verify building data
building = message["building"]
assert building["type"] == "small_house"
assert building["x"] == 3
assert building["y"] == 3
assert building["owner_id"] == client.player_data["player_id"]
@pytest.mark.asyncio
async def test_building_removal_through_websocket(self):
"""Test building removal state changes through WebSocket"""
async with test_clients("remove_test") as [client]:
# Place building first
await client.place_building("small_house", 8, 8)
placement_msg = await client.receive_message(timeout=2.0)
assert placement_msg["type"] == "building_placed"
# Remove building
await client.remove_building(8, 8)
# Wait for removal confirmation
message = await client.receive_message(timeout=2.0)
assert message["type"] == "building_removed"
assert message["x"] == 8
assert message["y"] == 8
@pytest.mark.asyncio
async def test_player_state_persistence(self):
"""Test that player state persists across connections"""
# First connection - place building and spend money
async with test_clients("persistence_test") as [client1]:
initial_money = client1.get_money()
await client1.place_building("small_house", 15, 15)
# Wait for the building to be placed and economy update
await asyncio.sleep(1.5)
# Reconnect with same nickname
async with test_clients("persistence_test") as [client2]:
# Money should be reduced by building cost
assert client2.get_money() < initial_money
expected_money = initial_money - 5000 # Small house cost
# Allow for economy ticks that might have occurred
money_difference = abs(client2.get_money() - expected_money)
assert money_difference <= 100 # Allow for small economy changes
@pytest.mark.asyncio
async def test_invalid_building_placement_error_handling(self):
"""Test error handling for invalid building placements"""
async with test_clients("error_test") as [client]:
# Place building on valid location
await client.place_building("small_house", 0, 0)
await client.receive_message(timeout=2.0) # Wait for placement
# Try to place another building on same location
await client.place_building("road", 0, 0)
# Should receive error message
message = await client.receive_message(timeout=2.0)
assert message["type"] == "error"
assert "occupied" in message["message"].lower()
@pytest.mark.asyncio
async def test_building_name_editing(self):
"""Test building name editing through WebSocket"""
async with test_clients("editor") as [client]:
# Place building
await client.place_building("small_house", 12, 12)
await client.receive_message(timeout=2.0) # Wait for placement
# Edit building name
new_name = "My Custom House"
await client.edit_building(12, 12, new_name)
# Should receive update confirmation
message = await client.receive_message(timeout=2.0)
assert message["type"] == "building_updated"
assert message["x"] == 12
assert message["y"] == 12
assert message["name"] == new_name