clovis 1 miesiąc temu
rodzic
commit
ba53acbd71

+ 1 - 1
src/assets/ClockTimeline/TimelineRow.tsx

@@ -69,7 +69,7 @@ export default function TimelineRow({ rowId, step }: { rowId: string; step?: num
       rowId,
       presetId: preset.id,
       start: tick,
-      duration: preset.ticks + 1,
+      duration: preset.ticks,
       count: 1,
     };
 

+ 0 - 0
src/engine/simulator.next.ts


+ 104 - 41
src/engine/simulator.test.ts

@@ -42,22 +42,26 @@ describe("Simulator Engine", () => {
     expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
 
     // 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
 
     // Tick 30: Craft completes!
     machine.tick();
     expect(machine.outputBuffer["iron-gear-wheel"]).toBe(1);
+    expect(machine.inputBuffer["iron-plate"]).toBe(0);
 
     machine.tick();
     // Tick 31: It should immediately start the next craft in the same tick if buffer allows
     expect(machine.isCrafting).toBe(true);
     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", () => {
     const fastSetup: MachineSetup = {
-      machine: { crafting_speed: 60 } as Machine, // 1 craft per tick!
+      machine: { crafting_speed: 60 } as Machine,
       machineModules: [],
       beacons: [],
       machineQualityLevel: 0,
@@ -69,7 +73,7 @@ describe("Simulator Engine", () => {
     // Tick 1: Should complete 2 crafts instantly (60 speed / 30 ticks required = 2 per 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);
   });
 
@@ -161,20 +165,31 @@ describe("Factorio Strict Phase Orchestrator", () => {
     orchestrator.registerInserter(outputInserter);
     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);
 
     // 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 (Machine Phase): Machine sees item, crafts (1/30).
     // ...
     // 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", () => {
@@ -257,7 +272,7 @@ describe("Factorio Strict Phase Orchestrator", () => {
     orchestrator.registerMachine(machine);
 
     // 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.
     // 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", () => {
     const orchestrator = new FactorioEngineOrchestrator();
+    const stackSizes = {
+      "copper-cable": 200,
+      "electronic-circuit": 200,
+    };
 
     const copperCableRecipe: Recipe = {
       name: "copper-cable",
@@ -640,12 +659,12 @@ describe("Factorio Strict Phase Orchestrator", () => {
       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 sourceIron = new Chest("iron-plate");
@@ -655,27 +674,21 @@ describe("Factorio Strict Phase Orchestrator", () => {
     // Cycle: 480 ticks. 16 crafts 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.
     orchestrator.registerRow(
       "row_mid_outer",
       [
-        { start: 200, end: 208 },
-        { start: 400, end: 408 },
+        { start: 1, end: 10 },
+        { start: 321, end: 329 },
       ],
       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)
     const inCop1 = new InserterSimulator(16, sourceCopper, cop1, "copper-plate");
@@ -695,8 +708,8 @@ describe("Factorio Strict Phase Orchestrator", () => {
     // Bind to Pub/Sub Rows
     [inCop1, inCop2, inCop3, inIron1, inIron2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_inputs"));
     [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"));
 
     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", () => {
     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 sortingChest = new Chest();
-
-    // 2. Setup the Filter Inserter
     const filterInserter = new FilterableInserterSimulator(16, mixedBelt, sortingChest);
-
-    // Configure it to use Circuit Network filters
     filterInserter.useDynamicFilters = true;
 
-    // 3. Fake the Circuit Network Pub/Sub (Forcing Copper initially)
     filterInserter.updateDynamicFilters(["copper-plate"]);
 
     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);
-
-    // It should have extracted Copper, but completely ignored the Iron!
     expect(mixedBelt.extractedCounts["copper-plate"]).toBeGreaterThan(0);
     expect(mixedBelt.extractedCounts["iron-plate"]).toBeUndefined();
     expect(sortingChest.receivedCounts["copper-plate"]).toBeGreaterThan(0);
     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"]);
 
     // Run for another 100 ticks
@@ -841,8 +848,64 @@ describe("Factorio Strict Phase Orchestrator", () => {
 
     // Run the simulation
     // 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);
 
     // --- VALIDATION ---

+ 33 - 29
src/engine/simulator.ts

@@ -102,34 +102,40 @@ export class MachineSimulator implements IContainer {
   public tick() {
     let progressRemaining = this.progressPerTick;
 
-    while (progressRemaining > 0) {
-      if (!this.isCrafting) {
-        if (this.hasEnoughInputs()) {
-          this.consumeInputs();
-          this.isCrafting = true;
-        } else {
-          break;
-        }
+    if (!this.isCrafting) {
+      if (this.hasEnoughInputs()) {
+        this.consumeInputs();
+        this.isCrafting = true;
+      } else {
+        return;
       }
+    }
+    while (progressRemaining > 0) {
+      const toFinish = 1.0 - this.craftProgress;
 
-      if (this.isCrafting) {
-        const progressToNextFinish = 1.0 - this.craftProgress;
-        const step = Math.min(progressRemaining, progressToNextFinish);
+      if (progressRemaining < toFinish) {
+        this.craftProgress += progressRemaining;
+        this.prodProgress += progressRemaining * this.timings.productivityBonus;
+        return; // Tick exhausted, exit.
+      }
 
-        this.craftProgress += step;
-        progressRemaining -= step;
-        this.prodProgress += step * this.timings.productivityBonus;
+      // The craft finishes THIS tick.
+      progressRemaining -= toFinish;
+      this.prodProgress += toFinish * this.timings.productivityBonus;
+      this.addResults(1);
+      this.craftProgress = 0.0;
 
-        if (this.craftProgress >= 0.99999) {
-          this.addResults(1);
-          this.craftProgress = 0;
-          this.isCrafting = false;
-        }
+      if (this.prodProgress >= 0.99999) {
+        const prodCrafts = Math.floor(this.prodProgress + 1e-5);
+        this.addResults(prodCrafts);
+        this.prodProgress -= prodCrafts;
+      }
 
-        while (this.prodProgress >= 0.99999) {
-          this.addResults(1);
-          this.prodProgress -= 1.0;
-        }
+      if (this.hasEnoughInputs()) {
+        this.consumeInputs();
+      } else {
+        this.isCrafting = false;
+        return;
       }
     }
   }
@@ -238,7 +244,7 @@ export class Belt implements IContainer {
   }
 
   public extract(itemId: string, maxAmount: number): number {
-    if (this.getAvailable(itemId)) {
+    if (this.getAvailable(itemId) > 0) {
       const amount = Math.min(maxAmount, 4);
       this.extractedCounts[itemId] = (this.extractedCounts[itemId] || 0) + amount;
       return amount;
@@ -302,7 +308,7 @@ export class InserterSimulator {
     switch (this.state) {
       case InserterState.Picking:
         if (this.isActive && this.toPick > 0) {
-          const picked = this.source.extract(this.targetItemId, this.toPick);
+          const picked = this.source.extract(this.currentTargetItem, this.toPick);
 
           if (picked > 0) {
             this.heldItems += picked;
@@ -326,7 +332,7 @@ export class InserterSimulator {
 
       case InserterState.Dropping:
         if (this.ticksInState >= this.dropTicks) {
-          this.destination.insert(this.targetItemId, this.heldItems);
+          this.destination.insert(this.currentTargetItem, this.heldItems);
 
           this.heldItems = 0;
           this.swingCount++;
@@ -368,14 +374,12 @@ export class FilterableInserterSimulator extends InserterSimulator {
     this.dynamicFilters = filters;
 
     // FACTORIO RULE: Partial Hand Eviction on Filter Change
-    if (this.state === InserterState.Picking && this.useDynamicFilters) {
+    if ((this.state === InserterState.Picking || this.state == InserterState.Idle) && this.useDynamicFilters) {
       if (!this.dynamicFilters.includes(this.currentTargetItem)) {
         if (this.heldItems > 0) {
-          // Force it to swing forward with the partial hand
           this.needed = 0;
           this.toPick = 0;
         } else {
-          // If empty, abort the pick and return to Idle
           this.state = InserterState.Idle;
           this.ticksInState = 0;
         }