Fix production batch cycle input deduction and downstream stock reservation
This commit is contained in:
parent
007fee47d4
commit
032b0cc255
3 changed files with 119 additions and 70 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue