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['totalProduced'] = p_state.get('totalProduced', 0.0) + produced
else:
# Manufacturing logic with input batch reservation
# Manufacturing logic with clean batch cycle state
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 not can_start:
p_state['isBottlenecked'] = True
else:
can_start = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed)
if can_start:
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'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt)
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['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)
if logistics > 0 and p_state.get('stock', 0.0) > 0:
max_sell = logistics * 0.75 * dt
sold = min(p_state['stock'], max_sell)
p_state['stock'] -= sold
reserved_stock = 0.0
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']))
earned = sold * p_cfg['basePrice'] * logistics_price_mult
state['cash'] = state.get('cash', 0.0) + earned
state['lifetimeRevenue'] = state.get('lifetimeRevenue', 0.0) + earned
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(

View file

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

View file

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