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