Fix production batch cycle input deduction and downstream stock reservation

This commit is contained in:
Christian 2026-10-16 00:17:20 +02:00
parent 007fee47d4
commit 032b0cc255
3 changed files with 119 additions and 70 deletions

View file

@ -269,44 +269,58 @@ def process_offline_and_ticks(state: dict) -> dict:
p_state['stock'] = p_state.get('stock', 0.0) + produced p_state['stock'] = p_state.get('stock', 0.0) + produced
p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced
else: else:
# Manufacturing logic with input batch reservation # Manufacturing logic with clean batch cycle state
needed = [(inp['id'], inp['amount'] * lines) for inp in inputs] needed = [(inp['id'], inp['amount'] * lines) for inp in inputs]
can_start = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed)
if p_state.get('cycleProgress', 0.0) <= 0.0: if p_state.get('cycleProgress', 0.0) <= 0.0:
if not can_start: can_start = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed)
p_state['isBottlenecked'] = True if can_start:
else:
p_state['isBottlenecked'] = False p_state['isBottlenecked'] = False
for item_id, req in needed: for item_id, req in needed:
state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req) state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req)
p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) p_state['cycleProgress'] = min(1.0, cycle_rate * dt)
else:
p_state['isBottlenecked'] = True
else: else:
p_state['isBottlenecked'] = False p_state['isBottlenecked'] = False
p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt)
if p_state['cycleProgress'] >= 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_start:
for item_id, req in needed:
state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req)
else:
p_state['isBottlenecked'] = True
# Passive Logistics Sales 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
can_start_next = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed)
if can_start_next:
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) logistics = p_state.get('logistics', 0)
if logistics > 0 and p_state.get('stock', 0.0) > 0: if logistics > 0 and p_state.get('stock', 0.0) > 0:
max_sell = logistics * 0.75 * dt reserved_stock = 0.0
sold = min(p_state['stock'], max_sell) for other_pid, other_cfg in PRODUCT_CONFIGS.items():
p_state['stock'] -= sold 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']))
earned = sold * p_cfg['basePrice'] * logistics_price_mult avail = max(0.0, p_state.get('stock', 0.0) - reserved_stock)
state['cash'] = state.get('cash', 0.0) + earned if avail > 0:
state['lifetimeRevenue'] = state.get('lifetimeRevenue', 0.0) + earned 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 # Update valuation
facility_assets = sum( facility_assets = sum(

View file

@ -68,9 +68,34 @@ def test_ipo_and_shares():
assert state['cash'] == 30.0 assert state['cash'] == 30.0
print("PASS: test_ipo_and_shares") 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']})")
if __name__ == '__main__': if __name__ == '__main__':
test_initial_state() test_initial_state()
test_raw_production_and_logistics() test_raw_production_and_logistics()
test_chained_dependency_and_bottleneck() test_chained_dependency_and_bottleneck()
test_chained_microchips_and_boards()
test_ipo_and_shares() test_ipo_and_shares()
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!") print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")

View file

@ -67,74 +67,84 @@ export class SimulationEngine {
} }
} }
} else { } else {
// Manufacturing logic with input batch reservation: // Manufacturing logic:
// Check if factory is currently executing an in-progress cycle or needs to start one // If cycle is ready to start (cycleProgress <= 0.0), check and reserve inputs.
// If already in progress, advance cycle until completion.
const neededInputs = p.inputs.map(inp => ({ const neededInputs = p.inputs.map(inp => ({
id: inp.id, id: inp.id,
amount: inp.amount * lines amount: inp.amount * lines
})); }));
const canStartBatch = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount);
if (pState.cycleProgress <= 0.0) { if (pState.cycleProgress <= 0.0) {
// Not currently running a cycle const canStart = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount);
if (!canStartBatch) { if (canStart) {
pState.isBottlenecked = true;
} else {
// Start batch by reserving required inputs
pState.isBottlenecked = false;
for (const inp of neededInputs) { for (const inp of neededInputs) {
if (state.products[inp.id]) { state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
}
} }
pState.cycleProgress += cycleRate * dt; pState.isBottlenecked = false;
pState.cycleProgress = Math.min(1.0, cycleRate * dt);
} else {
pState.isBottlenecked = true;
} }
} else { } else {
// Cycle already in progress (inputs already secured)
pState.isBottlenecked = false; pState.isBottlenecked = false;
pState.cycleProgress += cycleRate * dt; pState.cycleProgress += cycleRate * dt;
}
if (pState.cycleProgress >= 1.0) { if (pState.cycleProgress >= 1.0) {
// Finished cycle, deliver produced goods pState.cycleProgress = 0.0;
pState.cycleProgress = 0.0; const produced = p.outputAmount * lines;
const produced = p.outputAmount * lines; pState.stock += produced;
pState.stock += produced; pState.totalProduced = (pState.totalProduced || 0) + produced;
pState.totalProduced = (pState.totalProduced || 0) + produced; state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced; if (this._sectorAccumulators[p.sector]) {
if (this._sectorAccumulators[p.sector]) { this._sectorAccumulators[p.sector].units += produced;
this._sectorAccumulators[p.sector].units += produced; }
}
// Immediately check if next batch can start // Check if another batch can immediately start this tick
if (canStartBatch) { const canStartNext = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount);
for (const inp of neededInputs) { if (canStartNext) {
if (state.products[inp.id]) { for (const inp of neededInputs) {
state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount); state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
}
}
} else {
pState.isBottlenecked = true;
} }
pState.isBottlenecked = false;
} else {
pState.isBottlenecked = true;
} }
} }
} }
// Continuous Logistics Distribution (Wholesale Sales) // Continuous Logistics Distribution (Wholesale Sales)
// Intermediate goods required by active downstream lines have a reserved buffer so logistics won't starve them
const logisticsUnits = pState.logistics || 0; const logisticsUnits = pState.logistics || 0;
if (logisticsUnits > 0 && pState.stock > 0) { if (logisticsUnits > 0 && pState.stock > 0) {
// Logistics fleet capacity: 0.75 items/sec per unit // Check if any active unlocked facility needs this product as an input
const maxSellCapacity = logisticsUnits * 0.75 * dt; let reservedStock = 0;
const sold = Math.min(pState.stock, maxSellCapacity); for (const otherP of PRODUCTS) {
pState.stock -= sold; if (otherP.inputs && otherP.inputs.some(inp => inp.id === p.id)) {
const otherState = state.products[otherP.id];
if (otherState && otherState.unlocked && otherState.lines > 0) {
const inpDef = otherP.inputs.find(inp => inp.id === p.id);
reservedStock = Math.max(reservedStock, inpDef.amount * otherState.lines);
}
}
}
const salePrice = p.basePrice * logisticsPriceMult; const availableToSell = Math.max(0, pState.stock - reservedStock);
const earned = sold * salePrice; if (availableToSell > 0) {
state.cash += earned; // Logistics fleet capacity: 0.75 items/sec per unit
state.lifetimeRevenue += earned; const maxSellCapacity = logisticsUnits * 0.75 * dt;
tickRevenue += earned; const sold = Math.min(availableToSell, maxSellCapacity);
if (this._sectorAccumulators[p.sector]) { pState.stock -= sold;
this._sectorAccumulators[p.sector].rev += earned;
const salePrice = p.basePrice * logisticsPriceMult;
const earned = sold * salePrice;
state.cash += earned;
state.lifetimeRevenue += earned;
tickRevenue += earned;
if (this._sectorAccumulators[p.sector]) {
this._sectorAccumulators[p.sector].rev += earned;
}
} }
} }
} }