212 lines
9.1 KiB
Python
212 lines
9.1 KiB
Python
"""
|
|
Verification test suite for Production Backend Engine
|
|
"""
|
|
import sys
|
|
import time
|
|
import os
|
|
|
|
# Add backend directory to sys.path
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
import engine
|
|
|
|
def test_initial_state():
|
|
state = engine.get_initial_state()
|
|
assert state['cash'] == 30.0
|
|
assert 'wheat' in state['products']
|
|
assert state['products']['wheat']['unlocked'] is True
|
|
assert state['products']['wheat']['lines'] == 1
|
|
assert state['products']['flour']['unlocked'] is False
|
|
print("PASS: test_initial_state")
|
|
|
|
def test_raw_production_and_logistics():
|
|
state = engine.get_initial_state()
|
|
# Fast forward 10 seconds
|
|
state['lastTick'] = time.time() - 10.0
|
|
updated = engine.process_offline_and_ticks(state)
|
|
|
|
# Wheat has cycle time 1.0s, output 1, 1 line, plus logistics selling
|
|
assert updated['products']['wheat']['totalProduced'] > 0
|
|
assert updated['cash'] > 30.0
|
|
print(f"PASS: test_raw_production_and_logistics (Produced: {updated['products']['wheat']['totalProduced']}, Cash: {updated['cash']})")
|
|
|
|
def test_chained_dependency_and_bottleneck():
|
|
state = engine.get_initial_state()
|
|
# Turn off wheat lines to simulate 0 upstream production
|
|
state['products']['wheat']['lines'] = 0
|
|
state['products']['wheat']['stock'] = 0.0
|
|
|
|
# Unlock flour and give it 1 line
|
|
state['products']['flour']['unlocked'] = True
|
|
state['products']['flour']['lines'] = 1
|
|
|
|
# 1. Process with no wheat: flour should bottleneck
|
|
state['lastTick'] = time.time() - 5.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
assert state['products']['flour']['stock'] == 0.0
|
|
assert state['products']['flour']['isBottlenecked'] is True
|
|
|
|
# 2. Give 10 wheat: flour should consume wheat and produce flour
|
|
state['products']['wheat']['stock'] = 10.0
|
|
state['lastTick'] = time.time() - 4.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
assert state['products']['flour']['totalProduced'] > 0
|
|
assert state['products']['wheat']['stock'] < 10.0 # Consumed!
|
|
print("PASS: test_chained_dependency_and_bottleneck")
|
|
|
|
def test_ipo_and_shares():
|
|
state = engine.get_initial_state()
|
|
state['lifetimeRevenue'] = 400000.0 # $400k
|
|
earned = engine.calculate_shares_earned(state['lifetimeRevenue'])
|
|
assert earned == 20 # 10 * sqrt(400k / 100k) = 10 * 2 = 20
|
|
|
|
# Apply IPO action
|
|
state = engine.apply_actions(state, [{'type': 'TRIGGER_IPO'}])
|
|
assert state['ventureShares'] == 20
|
|
assert state['ipoCount'] == 1
|
|
assert state['products']['wheat']['lines'] == 1
|
|
assert state['cash'] == 30.0
|
|
print("PASS: test_ipo_and_shares")
|
|
|
|
def test_chained_microchips_and_boards():
|
|
state = engine.get_initial_state()
|
|
# Unlock silicon, polymers, microchips, circuit_boards
|
|
for pid in ['silicon', 'polymers', 'microchips', 'circuit_boards']:
|
|
state['products'][pid]['unlocked'] = True
|
|
state['products'][pid]['lines'] = 1
|
|
state['products'][pid]['stock'] = 0.0
|
|
|
|
# Give raw materials so silicon and polymers don't bottleneck
|
|
state['products']['silicon']['stock'] = 10.0
|
|
state['products']['polymers']['stock'] = 20.0
|
|
|
|
# Microchips has 1 logistics fleet selling, but circuit_boards needs 2 microchips
|
|
state['products']['microchips']['logistics'] = 1
|
|
|
|
# Simulate 30 seconds
|
|
state['lastTick'] = time.time() - 30.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
|
|
# Verify microchips produced and circuit_boards actually received microchips and produced boards!
|
|
assert state['products']['microchips']['totalProduced'] > 0
|
|
assert state['products']['circuit_boards']['totalProduced'] > 0
|
|
print(f"PASS: test_chained_microchips_and_boards (Chips: {state['products']['microchips']['totalProduced']}, Boards: {state['products']['circuit_boards']['totalProduced']})")
|
|
|
|
def test_policy_toggles():
|
|
state = engine.get_initial_state()
|
|
# Unlock wheat and flour
|
|
state['products']['wheat']['stock'] = 20.0
|
|
state['products']['flour']['unlocked'] = True
|
|
state['products']['flour']['lines'] = 1
|
|
|
|
# 1. Turn off downstream supply on wheat
|
|
state = engine.apply_actions(state, [{'type': 'TOGGLE_DOWNSTREAM_SUPPLY', 'productId': 'wheat'}])
|
|
assert state['products']['wheat']['supplyDownstreamEnabled'] is False
|
|
|
|
# Simulate 5 seconds: flour should be bottlenecked because wheat supply to downstream is blocked
|
|
state['lastTick'] = time.time() - 5.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
assert state['products']['flour']['stock'] == 0.0
|
|
assert state['products']['flour']['isBottlenecked'] is True
|
|
|
|
# 2. Turn downstream supply back on: flour consumes wheat and produces
|
|
state = engine.apply_actions(state, [{'type': 'TOGGLE_DOWNSTREAM_SUPPLY', 'productId': 'wheat'}])
|
|
assert state['products']['wheat']['supplyDownstreamEnabled'] is True
|
|
|
|
# 3. Turn off logistics fleet on wheat
|
|
state = engine.apply_actions(state, [{'type': 'TOGGLE_LOGISTICS', 'productId': 'wheat'}])
|
|
assert state['products']['wheat']['logisticsEnabled'] is False
|
|
|
|
# Simulate 5 seconds: wheat stock will NOT be sold by logistics fleet
|
|
cash_before = state['cash']
|
|
state['lastTick'] = time.time() - 5.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
assert state['cash'] == cash_before # Wheat logistics paused, no wholesale sales
|
|
assert state['products']['flour']['totalProduced'] > 0 # Flour received wheat!
|
|
print("PASS: test_policy_toggles")
|
|
|
|
def test_production_target_throttle():
|
|
state = engine.get_initial_state()
|
|
state['products']['wheat']['logisticsEnabled'] = False # Don't sell to isolate production rate
|
|
|
|
# Set throttle to 0.5 (50%)
|
|
state = engine.apply_actions(state, [{'type': 'SET_PRODUCTION_TARGET', 'productId': 'wheat', 'target': 0.5}])
|
|
assert state['products']['wheat']['productionTarget'] == 0.5
|
|
|
|
state['lastTick'] = time.time() - 10.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
produced_at_50 = state['products']['wheat']['totalProduced']
|
|
|
|
# Reset and test at 1.0 (100%)
|
|
state2 = engine.get_initial_state()
|
|
state2['products']['wheat']['logisticsEnabled'] = False
|
|
state2['lastTick'] = time.time() - 10.0
|
|
state2 = engine.process_offline_and_ticks(state2)
|
|
produced_at_100 = state2['products']['wheat']['totalProduced']
|
|
|
|
# 50% should produce roughly half (5 units vs 10 units over 10s at 1.0s base cycle)
|
|
assert produced_at_50 < produced_at_100
|
|
assert abs(produced_at_50 - (produced_at_100 * 0.5)) <= 1.0
|
|
|
|
# Set throttle to 0.0 (Paused / Off)
|
|
state = engine.apply_actions(state, [{'type': 'SET_PRODUCTION_TARGET', 'productId': 'wheat', 'target': 0.0}])
|
|
stock_before = state['products']['wheat']['stock']
|
|
state['lastTick'] = time.time() - 10.0
|
|
state = engine.process_offline_and_ticks(state)
|
|
assert state['products']['wheat']['stock'] == stock_before # Zero production when throttled to 0%
|
|
print(f"PASS: test_production_target_throttle (50%: {produced_at_50}, 100%: {produced_at_100})")
|
|
|
|
def test_reputation_allocation_and_retention():
|
|
state = engine.get_initial_state()
|
|
state['reputation'] = 100
|
|
|
|
# Allocate 30 to B2B, 40 to clients, 20 to brand
|
|
actions = [
|
|
{'type': 'ALLOCATE_REP', 'department': 'b2b', 'delta': 30},
|
|
{'type': 'ALLOCATE_REP', 'department': 'clients', 'delta': 40},
|
|
{'type': 'ALLOCATE_REP', 'department': 'brand', 'delta': 20}
|
|
]
|
|
state = engine.apply_actions(state, actions)
|
|
assert state['reputation'] == 10
|
|
assert state['repAllocations']['b2b'] == 30
|
|
assert state['repAllocations']['clients'] == 40
|
|
assert state['repAllocations']['brand'] == 20
|
|
|
|
# Test Titan discount on lines (40 * 0.2% = 8% discount)
|
|
p_cfg = engine.PRODUCT_CONFIGS['flour']
|
|
cost_base = p_cfg['baseFacilityCost']
|
|
expected_cost = cost_base * 0.92
|
|
state['cash'] = expected_cost + 10.0
|
|
cash_before = state['cash']
|
|
state = engine.apply_actions(state, [{'type': 'BUY_LINES', 'productId': 'flour'}])
|
|
assert abs(state['cash'] - (cash_before - expected_cost)) < 0.01
|
|
|
|
# Reset test
|
|
state = engine.apply_actions(state, [{'type': 'ALLOCATE_REP', 'delta': 'reset'}])
|
|
assert state['reputation'] == 100
|
|
assert state['repAllocations']['b2b'] == 0
|
|
assert state['repAllocations']['clients'] == 0
|
|
assert state['repAllocations']['brand'] == 0
|
|
|
|
# IPO without brand_legacy perk -> 0 rep preserved
|
|
state['lifetimeRevenue'] = 400000.0
|
|
state_no_perk = engine.apply_actions(dict(state), [{'type': 'TRIGGER_IPO'}])
|
|
assert state_no_perk['reputation'] == 0
|
|
|
|
# IPO with brand_legacy level 3 -> 30% rep preserved (30 Rep)
|
|
state['boardroomPerks']['brand_legacy'] = 3
|
|
state_with_perk = engine.apply_actions(dict(state), [{'type': 'TRIGGER_IPO'}])
|
|
assert state_with_perk['reputation'] == 30
|
|
assert state_with_perk['repAllocations'] == {'b2b': 0, 'clients': 0, 'brand': 0}
|
|
print("PASS: test_reputation_allocation_and_retention")
|
|
|
|
if __name__ == '__main__':
|
|
test_initial_state()
|
|
test_raw_production_and_logistics()
|
|
test_chained_dependency_and_bottleneck()
|
|
test_chained_microchips_and_boards()
|
|
test_policy_toggles()
|
|
test_production_target_throttle()
|
|
test_reputation_allocation_and_retention()
|
|
test_ipo_and_shares()
|
|
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")
|