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