""" Economy system tests - validates building costs, income calculations, road connectivity bonuses, and offline processing """ import pytest import asyncio from tests.test_client import TestWebSocketClient, test_clients from server.models import BuildingType, BUILDING_CONFIGS from server.game_state import GameState from server.economy import EconomyEngine import time class TestEconomySystem: """Test the economy system calculations and behaviors""" def test_building_costs(self, isolated_game_components): """Test that building configurations have correct costs""" game_state, economy_engine, database = isolated_game_components player = game_state.get_or_create_player("test_player", "test_id_123") # Test known building costs assert BUILDING_CONFIGS[BuildingType.SMALL_HOUSE].cost == 5000 assert BUILDING_CONFIGS[BuildingType.ROAD].cost == 500 assert BUILDING_CONFIGS[BuildingType.POWER_PLANT].cost == 100000 assert BUILDING_CONFIGS[BuildingType.MALL].cost == 80000 # Test that player starts with enough money assert player.money == 100000 assert player.can_afford(BUILDING_CONFIGS[BuildingType.SMALL_HOUSE].cost) # Player can afford power plant with starting money assert player.can_afford(BUILDING_CONFIGS[BuildingType.POWER_PLANT].cost) def test_basic_building_placement_costs(self, isolated_game_components, unique_coordinates): """Test that placing buildings deducts correct money""" game_state, economy_engine, database = isolated_game_components player = game_state.get_or_create_player("test_player", "test_id_123") initial_money = player.money # Get unique coordinates x1, y1 = unique_coordinates(0) x2, y2 = unique_coordinates(1) # Place a small house (costs $5000) result = game_state.place_building("test_id_123", "small_house", x1, y1) assert result["success"] == True assert player.money == initial_money - 5000 # Place a road (costs $500) result = game_state.place_building("test_id_123", "road", x2, y2) assert result["success"] == True assert player.money == initial_money - 5000 - 500 def test_insufficient_funds(self, isolated_game_components, unique_coordinates): """Test that buildings can't be placed without enough money""" game_state, economy_engine, database = isolated_game_components player = game_state.get_or_create_player("test_player", "test_id_123") # Drain most money player.money = 1000 # Try to place expensive building x, y = unique_coordinates(0) result = game_state.place_building("test_id_123", "power_plant", x, y) assert result["success"] == False assert "Not enough money" in result["error"] assert player.money == 1000 # Money unchanged def test_population_requirements(self, game_setup): """Test that commercial buildings require population""" game_state, economy_engine, player = game_setup # Try to place shop without population assert player.population == 0 result = game_state.place_building("test_id_123", "small_shop", 0, 0) assert result["success"] == False assert "population" in result["error"].lower() # Add population via house game_state.place_building("test_id_123", "small_house", 1, 1) economy_engine.tick() # Update population assert player.population == 10 # Now shop should work result = game_state.place_building("test_id_123", "small_shop", 0, 0) assert result["success"] == True def test_power_requirements(self, game_setup): """Test that large buildings require power plant""" game_state, economy_engine, player = game_setup # Add enough money for power plant player.money = 200000 # Try to place large factory without power plant result = game_state.place_building("test_id_123", "large_factory", 0, 0) assert result["success"] == False assert "power plant" in result["error"].lower() # Place power plant first game_state.place_building("test_id_123", "power_plant", 5, 5) # Now large factory should work result = game_state.place_building("test_id_123", "large_factory", 0, 0) assert result["success"] == True def test_basic_economy_tick(self, game_setup): """Test basic income/expense calculations""" game_state, economy_engine, player = game_setup initial_money = player.money # Place income-generating building game_state.place_building("test_id_123", "small_shop", 0, 0) # Place population-providing building game_state.place_building("test_id_123", "small_house", 1, 1) money_after_placement = player.money # Run economy tick economy_engine.tick() # Small shop should generate +$100, small house costs -$50 expected_change = 100 - 50 # +$50 net assert player.money == money_after_placement + expected_change def test_road_connectivity_bonus(self, game_setup): """Test that road connectivity provides economy bonuses""" game_state, economy_engine, player = game_setup # Place shop and house with population game_state.place_building("test_id_123", "small_shop", 2, 2) game_state.place_building("test_id_123", "small_house", 3, 3) economy_engine.tick() money_without_roads = player.money # Place roads near the shop to create connectivity game_state.place_building("test_id_123", "road", 2, 1) # Adjacent to shop game_state.place_building("test_id_123", "road", 2, 3) # Connected road game_state.place_building("test_id_123", "road", 1, 3) # Extend network economy_engine.tick() money_with_roads = player.money # Income should be higher with road connectivity # 3 roads = 15% bonus on shop income (100 * 1.15 = 115 vs 100) income_increase = money_with_roads - money_without_roads assert income_increase > 50 # Base income difference assert income_increase > 65 # Should be higher due to bonus def test_offline_economy_processing(self, game_setup): """Test that offline players get reduced income""" game_state, economy_engine, player = game_setup # Place income buildings game_state.place_building("test_id_123", "small_shop", 0, 0) game_state.place_building("test_id_123", "small_house", 1, 1) # Test online income player.is_online = True money_before_online = player.money economy_engine.tick() online_income = player.money - money_before_online # Test offline income (should be 10% of online) player.is_online = False money_before_offline = player.money economy_engine.tick() offline_income = player.money - money_before_offline # Offline income should be ~10% of online income expected_offline_income = int(online_income * 0.1) assert abs(offline_income - expected_offline_income) <= 1 # Allow rounding difference assert offline_income < online_income def test_negative_money_limit(self, game_setup): """Test that player money doesn't go below -$100,000""" game_state, economy_engine, player = game_setup # Set money near limit and place expensive buildings player.money = -99000 # Place many expensive buildings to drain money for i in range(10): game_state.place_building("test_id_123", "small_house", i, i) economy_engine.tick() # Money should not go below -$100,000 assert player.money >= -100000 def test_building_stats_calculation(self, game_setup): """Test building stats calculation for UI""" game_state, economy_engine, player = game_setup stats = economy_engine.calculate_building_stats("test_id_123", BuildingType.SMALL_SHOP) expected_config = BUILDING_CONFIGS[BuildingType.SMALL_SHOP] assert stats["cost"] == expected_config.cost assert stats["income"] == expected_config.income assert stats["population"] == expected_config.population assert stats["power_required"] == expected_config.power_required assert stats["requires_population"] == expected_config.requires_population class TestEconomyIntegration: """Integration tests for economy with WebSocket clients""" @pytest.mark.asyncio async def test_economy_through_websocket(self): """Test economy system through WebSocket client actions""" async with test_clients("economy_test") as [client]: initial_money = client.get_money() assert initial_money == 100000 # Starting money # Place a building that costs money await client.place_building("small_house", 0, 0) # Wait for server response message = await client.receive_message(timeout=2.0) assert message["type"] == "building_placed" # Wait for economy update stats_message = await client.receive_message(timeout=2.0) if stats_message and stats_message["type"] == "player_stats_update": # Money should be deducted new_money = stats_message["player"]["money"] assert new_money == initial_money - 5000 # Small house costs $5000 @pytest.mark.asyncio async def test_road_network_economy_bonus(self): """Test road network bonuses through WebSocket""" async with test_clients("road_test") as [client]: # Place shop and house for population await client.place_building("small_house", 0, 0) await asyncio.sleep(0.1) await client.place_building("small_shop", 1, 0) await asyncio.sleep(0.1) # Clear messages and wait for economy tick client.clear_messages() # Build road network await client.place_building("road", 0, 1) await client.place_building("road", 1, 1) await client.place_building("road", 2, 1) # Wait for economy updates messages = await client.receive_messages_for(3.0) # Find the latest player stats update stats_updates = [m for m in messages if m["type"] == "player_stats_update"] assert len(stats_updates) > 0 # With road connectivity, income should be higher than base # This is hard to test precisely due to timing, but we can verify # the economy system is working final_money = stats_updates[-1]["player"]["money"] assert final_money is not None @pytest.mark.asyncio async def test_building_requirements_through_websocket(self): """Test building requirements validation through WebSocket""" async with test_clients("requirements_test") as [client]: # Try to place shop without population - should fail await client.place_building("small_shop", 0, 0) message = await client.receive_message(timeout=2.0) assert message["type"] == "error" assert "population" in message["message"].lower() # Place house first to get population await client.place_building("small_house", 1, 1) building_msg = await client.receive_message(timeout=2.0) assert building_msg["type"] == "building_placed" # Wait for economy update to set population await asyncio.sleep(1.5) # Now shop should work await client.place_building("small_shop", 0, 0) success_msg = await client.receive_message(timeout=2.0) assert success_msg["type"] == "building_placed"