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