|
@@ -42,22 +42,26 @@ describe("Simulator Engine", () => {
|
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
|
|
|
|
|
|
|
|
// Fast forward 28 ticks (29 total)
|
|
// Fast forward 28 ticks (29 total)
|
|
|
- for (let i = 0; i < 28; i++) machine.tick();
|
|
|
|
|
|
|
+ for (let i = 0; i < 29; i++) machine.tick();
|
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0); // Not done yet
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0); // Not done yet
|
|
|
|
|
|
|
|
// Tick 30: Craft completes!
|
|
// Tick 30: Craft completes!
|
|
|
machine.tick();
|
|
machine.tick();
|
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(1);
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(1);
|
|
|
|
|
+ expect(machine.inputBuffer["iron-plate"]).toBe(0);
|
|
|
|
|
|
|
|
machine.tick();
|
|
machine.tick();
|
|
|
// Tick 31: It should immediately start the next craft in the same tick if buffer allows
|
|
// Tick 31: It should immediately start the next craft in the same tick if buffer allows
|
|
|
expect(machine.isCrafting).toBe(true);
|
|
expect(machine.isCrafting).toBe(true);
|
|
|
expect(machine.inputBuffer["iron-plate"]).toBe(0);
|
|
expect(machine.inputBuffer["iron-plate"]).toBe(0);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < 29; i++) machine.tick();
|
|
|
|
|
+ expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it("Machine Simulator: Processes multiple crafts in a single tick if speed is extreme", () => {
|
|
it("Machine Simulator: Processes multiple crafts in a single tick if speed is extreme", () => {
|
|
|
const fastSetup: MachineSetup = {
|
|
const fastSetup: MachineSetup = {
|
|
|
- machine: { crafting_speed: 60 } as Machine, // 1 craft per tick!
|
|
|
|
|
|
|
+ machine: { crafting_speed: 60 } as Machine,
|
|
|
machineModules: [],
|
|
machineModules: [],
|
|
|
beacons: [],
|
|
beacons: [],
|
|
|
machineQualityLevel: 0,
|
|
machineQualityLevel: 0,
|
|
@@ -69,7 +73,7 @@ describe("Simulator Engine", () => {
|
|
|
// Tick 1: Should complete 2 crafts instantly (60 speed / 30 ticks required = 2 per tick)
|
|
// Tick 1: Should complete 2 crafts instantly (60 speed / 30 ticks required = 2 per tick)
|
|
|
machine.tick();
|
|
machine.tick();
|
|
|
|
|
|
|
|
- expect(machine.inputBuffer["iron-plate"]).toBe(16); // 20 - 4
|
|
|
|
|
|
|
+ expect(machine.inputBuffer["iron-plate"]).toBe(14); // 20 - 6 (2 first craft plus next tick craft)
|
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
|
|
expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -161,20 +165,31 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
orchestrator.registerInserter(outputInserter);
|
|
orchestrator.registerInserter(outputInserter);
|
|
|
orchestrator.registerMachine(machine);
|
|
orchestrator.registerMachine(machine);
|
|
|
|
|
|
|
|
|
|
+ expect(orchestrator.currentTick).toBe(0);
|
|
|
|
|
+
|
|
|
|
|
+ orchestrator.tickUntil(() => inputInserter.state == InserterState.Dropping);
|
|
|
|
|
+ expect(orchestrator.currentTick).toBe(4);
|
|
|
|
|
+
|
|
|
|
|
+ orchestrator.tickUntil(() => (machine.outputBuffer["iron-gear-wheel"] || 0) >= 1);
|
|
|
|
|
+ expect(orchestrator.currentTick).toBe(35);
|
|
|
|
|
+
|
|
|
|
|
+ orchestrator.tickUntil(() => outputInserter.heldItems >= 1);
|
|
|
|
|
+ expect(orchestrator.currentTick).toBe(36);
|
|
|
|
|
+
|
|
|
orchestrator.tickUntil(() => (sinkChest.receivedCounts["iron-gear-wheel"] || 0) >= 1);
|
|
orchestrator.tickUntil(() => (sinkChest.receivedCounts["iron-gear-wheel"] || 0) >= 1);
|
|
|
|
|
|
|
|
// EXACT TIMELINE TRACE:
|
|
// EXACT TIMELINE TRACE:
|
|
|
- // T1: Input Picks.
|
|
|
|
|
- // T2-T4: Input Swings.
|
|
|
|
|
|
|
+ // T0: Input Picks.
|
|
|
|
|
+ // T1-T4: Input Swings.
|
|
|
// T5 (Inserter Phase): Input Drops item into machine. Output is asleep.
|
|
// T5 (Inserter Phase): Input Drops item into machine. Output is asleep.
|
|
|
// T5 (Machine Phase): Machine sees item, crafts (1/30).
|
|
// T5 (Machine Phase): Machine sees item, crafts (1/30).
|
|
|
// ...
|
|
// ...
|
|
|
// T34 (Machine Phase): Craft finishes (30/30). Item is output!
|
|
// T34 (Machine Phase): Craft finishes (30/30). Item is output!
|
|
|
- // T35 (Inserter Phase): Output finally sees item, wakes up, Picks. (1 TICK DELAY PROVEN!)
|
|
|
|
|
- // T36-38: Output Swings.
|
|
|
|
|
- // T39: Output Drops item into chest.
|
|
|
|
|
|
|
+ // T36 (Inserter Phase): Output finally sees item, wakes up, Picks.
|
|
|
|
|
+ // T37-39: Output Swings.
|
|
|
|
|
+ // T40: Output Drops item into chest.
|
|
|
|
|
|
|
|
- expect(orchestrator.currentTick).toBe(39);
|
|
|
|
|
|
|
+ expect(orchestrator.currentTick).toBe(40);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it("Round-Robin: Two inserters compete for same output and share perfectly", () => {
|
|
it("Round-Robin: Two inserters compete for same output and share perfectly", () => {
|
|
@@ -257,7 +272,7 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
orchestrator.registerMachine(machine);
|
|
orchestrator.registerMachine(machine);
|
|
|
|
|
|
|
|
// Run until 4 items are produced and extracted
|
|
// Run until 4 items are produced and extracted
|
|
|
- orchestrator.tickUntil(() => false, 4 + 30 * 4 + 1);
|
|
|
|
|
|
|
+ orchestrator.tickUntil(() => false, 5 + 30 * 4 + 1);
|
|
|
|
|
|
|
|
// If build-order was absolute, A would have 4 and B would have 0.
|
|
// If build-order was absolute, A would have 4 and B would have 0.
|
|
|
// Because of the Round-Robin queue, they should have exactly 2 each!
|
|
// Because of the Round-Robin queue, they should have exactly 2 each!
|
|
@@ -615,6 +630,10 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
|
|
|
|
|
it("Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows", () => {
|
|
it("Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows", () => {
|
|
|
const orchestrator = new FactorioEngineOrchestrator();
|
|
const orchestrator = new FactorioEngineOrchestrator();
|
|
|
|
|
+ const stackSizes = {
|
|
|
|
|
+ "copper-cable": 200,
|
|
|
|
|
+ "electronic-circuit": 200,
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
const copperCableRecipe: Recipe = {
|
|
const copperCableRecipe: Recipe = {
|
|
|
name: "copper-cable",
|
|
name: "copper-cable",
|
|
@@ -640,12 +659,12 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
machineQualityLevel: 0,
|
|
machineQualityLevel: 0,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const cop1 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
|
|
|
|
|
- const cop2 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
|
|
|
|
|
- const cop3 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
|
|
|
|
|
|
|
+ const cop1 = new MachineSimulator(assemblerSetup, copperCableRecipe, stackSizes);
|
|
|
|
|
+ const cop2 = new MachineSimulator(assemblerSetup, copperCableRecipe, stackSizes);
|
|
|
|
|
+ const cop3 = new MachineSimulator(assemblerSetup, copperCableRecipe, stackSizes);
|
|
|
|
|
|
|
|
- const circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { "electronic-circuit": 200 });
|
|
|
|
|
- const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { "electronic-circuit": 200 });
|
|
|
|
|
|
|
+ const circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, stackSizes);
|
|
|
|
|
+ const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, stackSizes);
|
|
|
|
|
|
|
|
const sourceCopper = new Chest("copper-plate");
|
|
const sourceCopper = new Chest("copper-plate");
|
|
|
const sourceIron = new Chest("iron-plate");
|
|
const sourceIron = new Chest("iron-plate");
|
|
@@ -655,27 +674,21 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
// Cycle: 480 ticks. 16 crafts per cycle.
|
|
// Cycle: 480 ticks. 16 crafts per cycle.
|
|
|
|
|
|
|
|
// Inputs (Copper/Iron): 16 items needed = 1 swing per cycle.
|
|
// Inputs (Copper/Iron): 16 items needed = 1 swing per cycle.
|
|
|
- orchestrator.registerRow("row_inputs", [{ start: 0, end: 8 }], 480);
|
|
|
|
|
|
|
+ orchestrator.registerRow("row_inputs", [{ start: 1, end: 9 }], 480);
|
|
|
|
|
|
|
|
// Outer Mid Inserters (cop1 -> circ1, cop3 -> circ2): 32 cables generated = 2 swings per cycle.
|
|
// Outer Mid Inserters (cop1 -> circ1, cop3 -> circ2): 32 cables generated = 2 swings per cycle.
|
|
|
orchestrator.registerRow(
|
|
orchestrator.registerRow(
|
|
|
"row_mid_outer",
|
|
"row_mid_outer",
|
|
|
[
|
|
[
|
|
|
- { start: 200, end: 208 },
|
|
|
|
|
- { start: 400, end: 408 },
|
|
|
|
|
|
|
+ { start: 1, end: 10 },
|
|
|
|
|
+ { start: 321, end: 329 },
|
|
|
],
|
|
],
|
|
|
480,
|
|
480,
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // Inner Mid Inserter A (cop2 -> circ1): 16 cables = 1 swing per cycle. Staggered to tick 120.
|
|
|
|
|
- orchestrator.registerRow("row_mid_inner_A", [{ start: 120, end: 128 }], 480);
|
|
|
|
|
-
|
|
|
|
|
- // Inner Mid Inserter B (cop2 -> circ2): 16 cables = 1 swing per cycle. Staggered to tick 360.
|
|
|
|
|
- // Because A and B are perfectly staggered, they will never fight over cop2's buffer!
|
|
|
|
|
- orchestrator.registerRow("row_mid_inner_B", [{ start: 360, end: 368 }], 480);
|
|
|
|
|
|
|
+ orchestrator.registerRow("row_mid_inner", [{ start: 161, end: 169 }], 480);
|
|
|
|
|
|
|
|
- // Outputs (Green Chips): 16 items generated = 1 swing per cycle.
|
|
|
|
|
- orchestrator.registerRow("row_outputs", [{ start: 460, end: 468 }], 480);
|
|
|
|
|
|
|
+ orchestrator.registerRow("row_outputs", [{ start: 0, end: 16 }], 480);
|
|
|
|
|
|
|
|
// Instantiate Inserters (Stack Size 16)
|
|
// Instantiate Inserters (Stack Size 16)
|
|
|
const inCop1 = new InserterSimulator(16, sourceCopper, cop1, "copper-plate");
|
|
const inCop1 = new InserterSimulator(16, sourceCopper, cop1, "copper-plate");
|
|
@@ -695,8 +708,8 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
// Bind to Pub/Sub Rows
|
|
// Bind to Pub/Sub Rows
|
|
|
[inCop1, inCop2, inCop3, inIron1, inIron2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_inputs"));
|
|
[inCop1, inCop2, inCop3, inIron1, inIron2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_inputs"));
|
|
|
[mid1, mid3].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_mid_outer"));
|
|
[mid1, mid3].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_mid_outer"));
|
|
|
- orchestrator.bindInserterToRow(mid2a, "row_mid_inner_A");
|
|
|
|
|
- orchestrator.bindInserterToRow(mid2b, "row_mid_inner_B");
|
|
|
|
|
|
|
+ orchestrator.bindInserterToRow(mid2a, "row_mid_inner");
|
|
|
|
|
+ orchestrator.bindInserterToRow(mid2b, "row_mid_inner");
|
|
|
[outCirc1, outCirc2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_outputs"));
|
|
[outCirc1, outCirc2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_outputs"));
|
|
|
|
|
|
|
|
const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
|
|
const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
|
|
@@ -746,32 +759,26 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
it("Advanced Filtering: Filter Inserter dynamically changes target on a mixed belt", () => {
|
|
it("Advanced Filtering: Filter Inserter dynamically changes target on a mixed belt", () => {
|
|
|
const orchestrator = new FactorioEngineOrchestrator();
|
|
const orchestrator = new FactorioEngineOrchestrator();
|
|
|
|
|
|
|
|
- // 1. Setup a Mixed Belt (simulated using our Belt class holding both)
|
|
|
|
|
const mixedBelt = new Belt("iron-plate", "copper-plate");
|
|
const mixedBelt = new Belt("iron-plate", "copper-plate");
|
|
|
const sortingChest = new Chest();
|
|
const sortingChest = new Chest();
|
|
|
-
|
|
|
|
|
- // 2. Setup the Filter Inserter
|
|
|
|
|
const filterInserter = new FilterableInserterSimulator(16, mixedBelt, sortingChest);
|
|
const filterInserter = new FilterableInserterSimulator(16, mixedBelt, sortingChest);
|
|
|
-
|
|
|
|
|
- // Configure it to use Circuit Network filters
|
|
|
|
|
filterInserter.useDynamicFilters = true;
|
|
filterInserter.useDynamicFilters = true;
|
|
|
|
|
|
|
|
- // 3. Fake the Circuit Network Pub/Sub (Forcing Copper initially)
|
|
|
|
|
filterInserter.updateDynamicFilters(["copper-plate"]);
|
|
filterInserter.updateDynamicFilters(["copper-plate"]);
|
|
|
|
|
|
|
|
orchestrator.registerInserter(filterInserter);
|
|
orchestrator.registerInserter(filterInserter);
|
|
|
|
|
+ orchestrator.tick();
|
|
|
|
|
|
|
|
- // --- PHASE 1: Pull Copper ---
|
|
|
|
|
|
|
+ expect(filterInserter.currentTargetItem).toBe("copper-plate");
|
|
|
|
|
+ expect(filterInserter.state).toBe(1);
|
|
|
|
|
+ expect(filterInserter.heldItems).toBe(4);
|
|
|
orchestrator.tickUntil(() => false, 100);
|
|
orchestrator.tickUntil(() => false, 100);
|
|
|
-
|
|
|
|
|
- // It should have extracted Copper, but completely ignored the Iron!
|
|
|
|
|
expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
|
|
expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
|
|
|
expect(mixedBelt.extractedCounts["iron-plate"]).toBeUndefined();
|
|
expect(mixedBelt.extractedCounts["iron-plate"]).toBeUndefined();
|
|
|
expect(sortingChest.receivedCounts["copper-plate"]).toBeGreaterThan(0);
|
|
expect(sortingChest.receivedCounts["copper-plate"]).toBeGreaterThan(0);
|
|
|
expect(sortingChest.receivedCounts["iron-plate"]).toBeUndefined();
|
|
expect(sortingChest.receivedCounts["iron-plate"]).toBeUndefined();
|
|
|
|
|
|
|
|
- // --- PHASE 2: The Combinator Flips! ---
|
|
|
|
|
- // The circuit network sends a new signal, changing the filter to Iron.
|
|
|
|
|
|
|
+ // The Combinator Flips! ---
|
|
|
filterInserter.updateDynamicFilters(["iron-plate"]);
|
|
filterInserter.updateDynamicFilters(["iron-plate"]);
|
|
|
|
|
|
|
|
// Run for another 100 ticks
|
|
// Run for another 100 ticks
|
|
@@ -841,8 +848,64 @@ describe("Factorio Strict Phase Orchestrator", () => {
|
|
|
|
|
|
|
|
// Run the simulation
|
|
// Run the simulation
|
|
|
// It takes 300 ticks for a single craft.
|
|
// It takes 300 ticks for a single craft.
|
|
|
- // We run it for 800 ticks, which gives enough time for 2 complete crafts and inserter swings.
|
|
|
|
|
- const targetTick = orchestrator.currentTick + 800;
|
|
|
|
|
|
|
+ const targetTick = orchestrator.currentTick + 300 * 5;
|
|
|
|
|
+ orchestrator.tickUntil(() => false, targetTick);
|
|
|
|
|
+
|
|
|
|
|
+ // --- VALIDATION ---
|
|
|
|
|
+
|
|
|
|
|
+ // The inserter MUST have extracted both items from the mixed belt.
|
|
|
|
|
+ // If it had locked up trying to stuff infinite copper, the gear count would be undefined/0.
|
|
|
|
|
+ expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
|
|
|
|
|
+ expect(mixedBelt.extractedCounts["iron-gear-wheel"]).toBeGreaterThan(0);
|
|
|
|
|
+
|
|
|
|
|
+ expect(sinkChest.receivedCounts["automation-science-pack"]).toBeGreaterThanOrEqual(2);
|
|
|
|
|
+
|
|
|
|
|
+ // The extracted copper should not wildly exceed the extracted gears,
|
|
|
|
|
+ // proving the machine's buffer choked the copper input and forced the inserter to switch.
|
|
|
|
|
+ // They should be reasonably close to each other (within 1 hand size margin)
|
|
|
|
|
+ const copperPulled = mixedBelt.extractedCounts["copper-plate"];
|
|
|
|
|
+ const gearsPulled = mixedBelt.extractedCounts["iron-gear-wheel"];
|
|
|
|
|
+ expect(Math.abs(copperPulled - gearsPulled)).toBeLessThanOrEqual(4);
|
|
|
|
|
+ });
|
|
|
|
|
+ it("Advanced dynamic Filtering: Single multi-filter inserter autonomously feeds a Red Science machine", () => {
|
|
|
|
|
+ const orchestrator = new FactorioEngineOrchestrator();
|
|
|
|
|
+
|
|
|
|
|
+ // The Red Science Recipe
|
|
|
|
|
+ const redScienceRecipe: Recipe = {
|
|
|
|
|
+ name: "automation-science-pack",
|
|
|
|
|
+ energy_required: 5, // 300 ticks per craft at Speed 1
|
|
|
|
|
+ ingredients: [
|
|
|
|
|
+ { name: "copper-plate", amount: 1, type: "item" },
|
|
|
|
|
+ { name: "iron-gear-wheel", amount: 1, type: "item" },
|
|
|
|
|
+ ],
|
|
|
|
|
+ results: [{ type: "item", name: "automation-science-pack", amount: 1 }],
|
|
|
|
|
+ } as Recipe;
|
|
|
|
|
+
|
|
|
|
|
+ const assemblerSetup: MachineSetup = {
|
|
|
|
|
+ machine: { name: "assembling-machine-2", crafting_speed: 1 } as Machine,
|
|
|
|
|
+ machineModules: [],
|
|
|
|
|
+ beacons: [],
|
|
|
|
|
+ machineQualityLevel: 0,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Setup the Machine and the Mixed Input Belt
|
|
|
|
|
+ const machine = new MachineSimulator(assemblerSetup, redScienceRecipe, { "automation-science-pack": 100 });
|
|
|
|
|
+ const mixedBelt = new Belt("copper-plate", "iron-gear-wheel");
|
|
|
|
|
+ const sinkChest = new Chest();
|
|
|
|
|
+
|
|
|
|
|
+ // A single Filter Inserter configured to grab BOTH ingredients!
|
|
|
|
|
+ // We use a small hand size (4) to force it to make multiple trips.
|
|
|
|
|
+ const smartInput = new FilterableInserterSimulator(4, mixedBelt, machine);
|
|
|
|
|
+ const output = new InserterSimulator(4, machine, sinkChest, "automation-science-pack");
|
|
|
|
|
+
|
|
|
|
|
+ orchestrator.registerInserter(smartInput);
|
|
|
|
|
+ orchestrator.registerMachine(machine);
|
|
|
|
|
+ orchestrator.registerInserter(output);
|
|
|
|
|
+ smartInput.updateDynamicFilters(["copper-plate", "iron-gear-wheel"]);
|
|
|
|
|
+ smartInput.useDynamicFilters = true;
|
|
|
|
|
+ // Run the simulation
|
|
|
|
|
+ // It takes 300 ticks for a single craft.
|
|
|
|
|
+ const targetTick = orchestrator.currentTick + 300 * 5;
|
|
|
orchestrator.tickUntil(() => false, targetTick);
|
|
orchestrator.tickUntil(() => false, targetTick);
|
|
|
|
|
|
|
|
// --- VALIDATION ---
|
|
// --- VALIDATION ---
|