""" Production - The Idle Game: Core Simulation Backend Engine Authoritative offline tick progression, multi-tier DAG supply chains, and action validation. """ import math import time # Initial Product Catalog configuration mirroring frontend/js/data/products.js PRODUCT_CONFIGS = { # Sector 1: Agriculture & Food 'wheat': { 'sector': 'agriculture', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 1.0, 'unlockCost': 0, 'baseFacilityCost': 15, 'baseManagerCost': 100, 'baseLogisticsCost': 80, 'basePrice': 1.50, 'unlockedDefault': True }, 'flour': { 'sector': 'agriculture', 'tier': 2, 'inputs': [{'id': 'wheat', 'amount': 2}], 'outputAmount': 1, 'baseCycleTime': 1.8, 'unlockCost': 250, 'baseFacilityCost': 200, 'baseManagerCost': 850, 'baseLogisticsCost': 600, 'basePrice': 6.20, 'unlockedDefault': False }, 'sugar_beet': { 'sector': 'agriculture', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 1.5, 'unlockCost': 650, 'baseFacilityCost': 550, 'baseManagerCost': 1800, 'baseLogisticsCost': 1200, 'basePrice': 4.80, 'unlockedDefault': False }, 'refined_sugar': { 'sector': 'agriculture', 'tier': 2, 'inputs': [{'id': 'sugar_beet', 'amount': 3}], 'outputAmount': 1, 'baseCycleTime': 2.2, 'unlockCost': 2200, 'baseFacilityCost': 1800, 'baseManagerCost': 4500, 'baseLogisticsCost': 3200, 'basePrice': 22.00, 'unlockedDefault': False }, 'fresh_bread': { 'sector': 'agriculture', 'tier': 3, 'inputs': [{'id': 'flour', 'amount': 2}, {'id': 'refined_sugar', 'amount': 1}], 'outputAmount': 2, 'baseCycleTime': 3.0, 'unlockCost': 9500, 'baseFacilityCost': 7500, 'baseManagerCost': 18000, 'baseLogisticsCost': 12500, 'basePrice': 58.00, 'unlockedDefault': False }, 'packaged_meals': { 'sector': 'agriculture', 'tier': 4, 'inputs': [{'id': 'fresh_bread', 'amount': 3}], 'outputAmount': 1, 'baseCycleTime': 4.5, 'unlockCost': 45000, 'baseFacilityCost': 36000, 'baseManagerCost': 75000, 'baseLogisticsCost': 55000, 'basePrice': 260.00, 'unlockedDefault': False }, # Sector 2: Forestry & Materials 'timber': { 'sector': 'materials', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 1.2, 'unlockCost': 120, 'baseFacilityCost': 95, 'baseManagerCost': 350, 'baseLogisticsCost': 240, 'basePrice': 2.80, 'unlockedDefault': False }, 'planks': { 'sector': 'materials', 'tier': 2, 'inputs': [{'id': 'timber', 'amount': 2}], 'outputAmount': 1, 'baseCycleTime': 2.0, 'unlockCost': 1200, 'baseFacilityCost': 950, 'baseManagerCost': 2800, 'baseLogisticsCost': 2000, 'basePrice': 11.50, 'unlockedDefault': False }, 'paper': { 'sector': 'materials', 'tier': 2, 'inputs': [{'id': 'timber', 'amount': 3}], 'outputAmount': 2, 'baseCycleTime': 2.5, 'unlockCost': 3500, 'baseFacilityCost': 2800, 'baseManagerCost': 6500, 'baseLogisticsCost': 4800, 'basePrice': 16.00, 'unlockedDefault': False }, 'furniture': { 'sector': 'materials', 'tier': 3, 'inputs': [{'id': 'planks', 'amount': 3}], 'outputAmount': 1, 'baseCycleTime': 3.8, 'unlockCost': 18000, 'baseFacilityCost': 14000, 'baseManagerCost': 35000, 'baseLogisticsCost': 26000, 'basePrice': 95.00, 'unlockedDefault': False }, # Sector 3: Mining & Metallurgy 'iron_ore': { 'sector': 'mining', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 1.6, 'unlockCost': 1500, 'baseFacilityCost': 1200, 'baseManagerCost': 3500, 'baseLogisticsCost': 2400, 'basePrice': 12.00, 'unlockedDefault': False }, 'coal': { 'sector': 'mining', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 1.8, 'unlockCost': 2800, 'baseFacilityCost': 2200, 'baseManagerCost': 5500, 'baseLogisticsCost': 3800, 'basePrice': 15.00, 'unlockedDefault': False }, 'steel_ingots': { 'sector': 'mining', 'tier': 2, 'inputs': [{'id': 'iron_ore', 'amount': 2}, {'id': 'coal', 'amount': 1}], 'outputAmount': 1, 'baseCycleTime': 3.2, 'unlockCost': 12500, 'baseFacilityCost': 9800, 'baseManagerCost': 24000, 'baseLogisticsCost': 17000, 'basePrice': 72.00, 'unlockedDefault': False }, 'metal_frames': { 'sector': 'mining', 'tier': 3, 'inputs': [{'id': 'steel_ingots', 'amount': 3}], 'outputAmount': 1, 'baseCycleTime': 4.6, 'unlockCost': 65000, 'baseFacilityCost': 52000, 'baseManagerCost': 110000, 'baseLogisticsCost': 80000, 'basePrice': 320.00, 'unlockedDefault': False }, # Sector 4: Chemicals & Polymers 'crude_oil': { 'sector': 'chemicals', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 2.0, 'unlockCost': 25000, 'baseFacilityCost': 20000, 'baseManagerCost': 48000, 'baseLogisticsCost': 35000, 'basePrice': 55.00, 'unlockedDefault': False }, 'silica_sand': { 'sector': 'chemicals', 'tier': 1, 'inputs': [], 'outputAmount': 1, 'baseCycleTime': 2.2, 'unlockCost': 45000, 'baseFacilityCost': 36000, 'baseManagerCost': 75000, 'baseLogisticsCost': 52000, 'basePrice': 85.00, 'unlockedDefault': False }, 'polymers': { 'sector': 'chemicals', 'tier': 2, 'inputs': [{'id': 'crude_oil', 'amount': 2}], 'outputAmount': 1, 'baseCycleTime': 3.5, 'unlockCost': 110000, 'baseFacilityCost': 88000, 'baseManagerCost': 175000, 'baseLogisticsCost': 120000, 'basePrice': 210.00, 'unlockedDefault': False }, 'silicon': { 'sector': 'chemicals', 'tier': 2, 'inputs': [{'id': 'silica_sand', 'amount': 2}], 'outputAmount': 1, 'baseCycleTime': 4.0, 'unlockCost': 220000, 'baseFacilityCost': 175000, 'baseManagerCost': 35000, 'baseLogisticsCost': 240000, 'basePrice': 420.00, 'unlockedDefault': False }, # Sector 5: High-Tech & Electronics 'microchips': { 'sector': 'electronics', 'tier': 3, 'inputs': [{'id': 'silicon', 'amount': 1}, {'id': 'polymers', 'amount': 1}], 'outputAmount': 2, 'baseCycleTime': 4.8, 'unlockCost': 550000, 'baseFacilityCost': 440000, 'baseManagerCost': 850000, 'baseLogisticsCost': 600000, 'basePrice': 650.00, 'unlockedDefault': False }, 'circuit_boards': { 'sector': 'electronics', 'tier': 3, 'inputs': [{'id': 'microchips', 'amount': 2}, {'id': 'polymers', 'amount': 2}], 'outputAmount': 1, 'baseCycleTime': 5.5, 'unlockCost': 1500000, 'baseFacilityCost': 1200000, 'baseManagerCost': 2400000, 'baseLogisticsCost': 1600000, 'basePrice': 2200.00, 'unlockedDefault': False }, 'smartphones': { 'sector': 'electronics', 'tier': 4, 'inputs': [{'id': 'circuit_boards', 'amount': 1}, {'id': 'polymers', 'amount': 2}], 'outputAmount': 1, 'baseCycleTime': 6.5, 'unlockCost': 5000000, 'baseFacilityCost': 4000000, 'baseManagerCost': 8000000, 'baseLogisticsCost': 5500000, 'basePrice': 6800.00, 'unlockedDefault': False }, # Sector 6: Heavy Industry & Vehicles 'engines': { 'sector': 'heavy_industry', 'tier': 3, 'inputs': [{'id': 'steel_ingots', 'amount': 3}, {'id': 'metal_frames', 'amount': 1}], 'outputAmount': 1, 'baseCycleTime': 6.0, 'unlockCost': 3500000, 'baseFacilityCost': 2800000, 'baseManagerCost': 5500000, 'baseLogisticsCost': 3800000, 'basePrice': 4200.00, 'unlockedDefault': False }, 'commercial_trucks': { 'sector': 'heavy_industry', 'tier': 4, 'inputs': [{'id': 'engines', 'amount': 1}, {'id': 'metal_frames', 'amount': 2}, {'id': 'circuit_boards', 'amount': 1}], 'outputAmount': 1, 'baseCycleTime': 8.5, 'unlockCost': 15000000, 'baseFacilityCost': 12000000, 'baseManagerCost': 22000000, 'baseLogisticsCost': 15000000, 'basePrice': 26500.00, 'unlockedDefault': False } } UNIT_MILESTONES = [1, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000] BOARDROOM_PERK_COSTS = { 'supply_sync': [1, 3, 8, 20, 50], 'robotics_overclock': [2, 5, 12, 30, 75], 'logistics_efficiency': [2, 6, 15, 40, 100], 'government_subsidies': [3, 8, 20, 55, 120], 'angel_syndicate': [5, 15, 45, 120, 300] } def clean_round(val: float) -> float: if math.isnan(val) or math.isinf(val) or val < 0: return 0.0 return round(float(val), 2) def get_unit_milestone_multiplier(count: int) -> float: reached = sum(1 for m in UNIT_MILESTONES if count >= m) return float(math.pow(2.0, reached)) def get_initial_state() -> dict: state = { 'version': 1, 'companyName': 'Apex Production Industries', 'cash': 30.0, 'lifetimeRevenue': 0.0, 'valuation': 30.0, 'ventureShares': 0, 'totalVentureShares': 0, 'ipoCount': 0, 'reputation': 0, 'buyMode': '1', 'activeTab': 'production', 'selectedSector': 'all', 'selectedProductId': 'wheat', 'stats': { 'totalProduced': 0.0, 'contractsCompleted': 0, 'managersHired': 0 }, 'products': {}, 'boardroomPerks': {p: 0 for p in BOARDROOM_PERK_COSTS}, 'achievements': {}, 'activeContracts': [], 'lastTick': time.time() } for pid, p in PRODUCT_CONFIGS.items(): is_unlocked = p.get('unlockedDefault', False) state['products'][pid] = { 'unlocked': is_unlocked, 'stock': 0.0, 'lines': 1 if is_unlocked else 0, 'manager': False, 'logistics': 1 if is_unlocked else 0, 'cycleProgress': 0.0, 'isBottlenecked': False, 'totalProduced': 0.0 } return state def calculate_shares_earned(lifetime_revenue: float) -> int: if lifetime_revenue < 100000.0: return 0 return int(math.floor(10.0 * math.sqrt(lifetime_revenue / 100000.0))) def process_offline_and_ticks(state: dict) -> dict: """Computes authoritative tick progress up to the current timestamp (capped at 8 hours).""" now = time.time() last = state.get('lastTick', now) elapsed = max(0.0, min(now - last, 8.0 * 3600.0)) state['lastTick'] = now if elapsed <= 0.1: return state perks = state.get('boardroomPerks', {}) supply_yield_mult = 1.0 + 0.15 * perks.get('supply_sync', 0) robotics_speed_mult = 1.0 + 0.20 * perks.get('robotics_overclock', 0) logistics_price_mult = 1.0 + 0.25 * perks.get('logistics_efficiency', 0) # Simulate in reasonable sub-steps for chained DAG dependency sub_steps = max(1, min(int(elapsed), 120)) dt = elapsed / sub_steps for _ in range(sub_steps): for pid, p_cfg in PRODUCT_CONFIGS.items(): p_state = state.get('products', {}).get(pid) if not p_state or not p_state.get('unlocked'): continue lines = p_state.get('lines', 0) if lines <= 0: continue milestone_mult = get_unit_milestone_multiplier(lines) cycle_rate = (1.0 / p_cfg['baseCycleTime']) * milestone_mult * robotics_speed_mult inputs = p_cfg.get('inputs', []) if not inputs: # Raw extraction p_state['isBottlenecked'] = False p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) if p_state['cycleProgress'] >= 1.0: cycles = int(p_state['cycleProgress']) p_state['cycleProgress'] -= cycles produced = cycles * p_cfg['outputAmount'] * lines * supply_yield_mult p_state['stock'] = p_state.get('stock', 0.0) + produced p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced else: # Manufacturing logic with clean batch cycle state needed = [(inp['id'], inp['amount'] * lines) for inp in inputs] def can_access_inputs(): for item_id, req in needed: upstream_state = state.get('products', {}).get(item_id, {}) if upstream_state.get('supplyDownstreamEnabled') is False: return False if upstream_state.get('stock', 0.0) < req: return False return True if p_state.get('cycleProgress', 0.0) <= 0.0: if can_access_inputs(): p_state['isBottlenecked'] = False for item_id, req in needed: state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req) p_state['cycleProgress'] = min(1.0, cycle_rate * dt) else: p_state['isBottlenecked'] = True else: p_state['isBottlenecked'] = False p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) if p_state.get('cycleProgress', 0.0) >= 1.0: p_state['cycleProgress'] = 0.0 produced = p_cfg['outputAmount'] * lines p_state['stock'] = p_state.get('stock', 0.0) + produced p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced # Check next batch if can_access_inputs(): for item_id, req in needed: state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req) p_state['isBottlenecked'] = False else: p_state['isBottlenecked'] = True # Passive Logistics Sales with stock reservation for downstream factories logistics = p_state.get('logistics', 0) is_logistics_active = p_state.get('logisticsEnabled', True) is not False if is_logistics_active and logistics > 0 and p_state.get('stock', 0.0) > 0: reserved_stock = 0.0 if p_state.get('supplyDownstreamEnabled', True) is not False: for other_pid, other_cfg in PRODUCT_CONFIGS.items(): other_inputs = other_cfg.get('inputs', []) if any(inp['id'] == pid for inp in other_inputs): other_state = state.get('products', {}).get(other_pid) if other_state and other_state.get('unlocked') and other_state.get('lines', 0) > 0: inp_match = next(inp for inp in other_inputs if inp['id'] == pid) reserved_stock = max(reserved_stock, float(inp_match['amount'] * other_state['lines'])) avail = max(0.0, p_state.get('stock', 0.0) - reserved_stock) if avail > 0: max_sell = logistics * 0.75 * dt sold = min(avail, max_sell) p_state['stock'] -= sold earned = sold * p_cfg['basePrice'] * logistics_price_mult state['cash'] = state.get('cash', 0.0) + earned state['lifetimeRevenue'] = state.get('lifetimeRevenue', 0.0) + earned # Update valuation facility_assets = sum( p_state.get('lines', 0) * PRODUCT_CONFIGS[pid]['baseFacilityCost'] for pid, p_state in state.get('products', {}).items() if pid in PRODUCT_CONFIGS ) state['valuation'] = state.get('cash', 0.0) + facility_assets + (state.get('lifetimeRevenue', 0.0) * 0.15) return state def apply_actions(state: dict, actions: list) -> dict: """Validates and executes queued actions sent from the frontend.""" for act in actions: atype = act.get('type') if atype == 'UNLOCK_PRODUCT': pid = act.get('productId') if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): p_cfg = PRODUCT_CONFIGS[pid] cost = p_cfg['unlockCost'] if state.get('cash', 0.0) >= cost and not state['products'][pid].get('unlocked'): state['cash'] -= cost state['products'][pid]['unlocked'] = True state['products'][pid]['lines'] = 1 elif atype == 'BUY_LINES': pid = act.get('productId') if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): p_cfg = PRODUCT_CONFIGS[pid] current_lines = state['products'][pid].get('lines', 0) cost = p_cfg['baseFacilityCost'] * math.pow(1.15, current_lines) if state.get('cash', 0.0) >= cost: state['cash'] -= cost state['products'][pid]['lines'] = current_lines + 1 elif atype == 'HIRE_MANAGER': pid = act.get('productId') if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): cost = PRODUCT_CONFIGS[pid]['baseManagerCost'] if state.get('cash', 0.0) >= cost and not state['products'][pid].get('manager'): state['cash'] -= cost state['products'][pid]['manager'] = True state['stats']['managersHired'] = state['stats'].get('managersHired', 0) + 1 elif atype == 'BUY_LOGISTICS': pid = act.get('productId') if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): current_logistics = state['products'][pid].get('logistics', 0) cost = PRODUCT_CONFIGS[pid]['baseLogisticsCost'] * math.pow(1.15, current_logistics) if state.get('cash', 0.0) >= cost: state['cash'] -= cost state['products'][pid]['logistics'] = current_logistics + 1 elif atype == 'TOGGLE_LOGISTICS': pid = act.get('productId') if pid in state.get('products', {}): current = state['products'][pid].get('logisticsEnabled', True) is not False state['products'][pid]['logisticsEnabled'] = not current elif atype == 'TOGGLE_DOWNSTREAM_SUPPLY': pid = act.get('productId') if pid in state.get('products', {}): current = state['products'][pid].get('supplyDownstreamEnabled', True) is not False state['products'][pid]['supplyDownstreamEnabled'] = not current elif atype == 'TRIGGER_IPO': earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0)) if earned > 0: preserved_shares = state.get('ventureShares', 0) + earned preserved_total_shares = state.get('totalVentureShares', 0) + earned preserved_perks = state.get('boardroomPerks', {}) preserved_ipos = state.get('ipoCount', 0) + 1 # Reset to clean initial state state = get_initial_state() state['ventureShares'] = preserved_shares state['totalVentureShares'] = preserved_total_shares state['boardroomPerks'] = preserved_perks state['ipoCount'] = preserved_ipos elif atype == 'BUY_PERK': perk_id = act.get('perkId') if perk_id in BOARDROOM_PERK_COSTS: current_lvl = state.get('boardroomPerks', {}).get(perk_id, 0) costs = BOARDROOM_PERK_COSTS[perk_id] if current_lvl < len(costs): req_shares = costs[current_lvl] if state.get('ventureShares', 0) >= req_shares: state['ventureShares'] -= req_shares state['boardroomPerks'][perk_id] = current_lvl + 1 return state def sanitize_state(raw: dict) -> dict: """Ensures incoming state doesn't have NaN or negative numbers.""" if not isinstance(raw, dict): return get_initial_state() return raw