JAQUIN_C 1 mesiac pred
rodič
commit
8c4c2a2c8a
1 zmenil súbory, kde vykonal 665 pridanie a 0 odobranie
  1. 665 0
      src/engine/simulator.next.ts

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

@@ -0,0 +1,665 @@
+// src/engine/simulator.ts
+
+export class InserterSimulator {
+  public state: InserterState = InserterState.Idle;
+  public ticksInState = 0;
+  public heldItems = 0;
+  
+  // NEW: Tracks how many full drops it has completed
+  public swingCount = 0; 
+  
+  // NEW: Controlled by the Circuit Network (Combinators)
+  public isActive = true; 
+
+// NEW: Mutable target so filter inserters can switch items!
+  public currentTargetItem: string;
+
+  private readonly pickupRate: number;
+  private readonly dropTicks: number;
+
+  constructor(
+    public handSize: number,
+    public source: IContainer,
+    public destination: IContainer,
+    public targetItemId: string,
+  ) {
+    this.currentTargetItem = targetItemId;
+    this.pickupRate = INSERTER_TIMINGS.PICKUP_RATE[source.type];
+    this.dropTicks = INSERTER_TIMINGS.DROP_DELAY[destination.type];
+  }
+
+  public tick() {
+    if (this.state === InserterState.Idle) {
+      if (this.canWakeUp()) {
+        this.state = InserterState.Picking;
+        this.ticksInState = 0;
+        
+        // Initialize caches
+        this.needed = this.handSize; 
+        this.toPick = Math.min(this.needed, this.pickupRate);
+      } else {
+        return;
+      }
+    }
+
+    this.ticksInState++;
+
+    switch (this.state) {
+      case InserterState.Picking:
+        // FASTEST PATH: Only extract if active and we know we need items
+        if (this.isActive && this.toPick > 0) {
+          const picked = this.source.extract(this.targetItemId, this.toPick);
+          
+          if (picked > 0) {
+            this.heldItems += picked;
+            this.needed -= picked;
+            // Update toPick cache ONLY when needed changes
+            this.toPick = Math.min(this.needed, this.pickupRate); 
+          }
+        }
+
+        // FAST EVALUATION: 0 is falsy, instantly transitions if full
+        if (!this.needed) {
+          this.state = InserterState.SwingingForward;
+          this.ticksInState = 0;
+        }
+        break;
+
+      case InserterState.SwingingForward:
+        if (this.ticksInState >= INSERTER_TIMINGS.ROTATION) {
+          this.state = InserterState.Dropping;
+          this.ticksInState = 0;
+        }
+        break;
+
+      case InserterState.Dropping:
+        if (!this.destination.canAccept(this.targetItemId)) {
+          this.ticksInState--; 
+          break;
+        }
+
+        if (this.ticksInState >= this.dropTicks) {
+          this.destination.insert(this.targetItemId, this.heldItems);
+          
+          this.heldItems = 0;
+          this.swingCount++; 
+          this.state = InserterState.SwingingBack;
+          this.ticksInState = 0;
+        }
+        break;
+
+      case InserterState.SwingingBack:
+        if (this.ticksInState >= INSERTER_TIMINGS.ROTATION) {
+          this.state = InserterState.Idle;
+          this.ticksInState = 0;
+        }
+        break;
+    }
+  }
+  protected canWakeUp(): boolean {
+    if (!this.isActive) return false;
+    const hasEnough = this.source.getAvailable(this.currentTargetItem) > 0;
+    return hasEnough && this.destination.canAccept(this.currentTargetItem);
+  }
+
+  // ... (keep canWakeUp unchanged)
+}
+export class FilterableInserterSimulator extends InserterSimulator {
+  public staticFilters: string[] = [];
+  
+  public useDynamicFilters = false;
+  public dynamicFilters: string[] = [];
+
+  constructor(
+    handSize: number,
+    source: IContainer,
+    destination: IContainer,
+    filters: string[] = [],
+  ) {
+    // Pass the first filter as a dummy fallback to super()
+    super(handSize, source, destination, filters[0] || '');
+    this.staticFilters = filters;
+  }
+
+  // Called by the Circuit Network (Pub/Sub)
+  public updateDynamicFilters(filters: string[]) {
+    this.dynamicFilters = filters;
+
+    // FACTORIO RULE: Partial Hand Eviction on Filter Change
+    if (this.state === InserterState.Picking && 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;
+        }
+      }
+    }
+  }
+
+  protected override canWakeUp(): boolean {
+    if (!this.isActive) return false;
+
+    const activeFilters = this.useDynamicFilters ? this.dynamicFilters : this.staticFilters;
+
+    // Scan the filters in order of priority (left to right in Factorio UI)
+    for (const itemId of activeFilters) {
+      if (this.source.getAvailable(itemId) > 0 && this.destination.canAccept(itemId)) {
+        
+        // LOCK ON: We found a valid item! Bind the hand to this item for the entire swing.
+        this.currentTargetItem = itemId; 
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
+// src/engine/simulator.ts
+
+export class OptimizedClockRow {
+  public rowId: string;
+  public isActive = false;
+  
+  // Maps exact local tick -> target active state (true/false)
+  private transitionMap = new Map<number, boolean>();
+  private subscribers: ((isActive: boolean) => void)[] = [];
+
+  constructor(
+    rowId: string, 
+    blocks: { start: number; end: number }[], 
+    public cycleDuration: number
+  ) {
+    this.rowId = rowId;
+
+    // 1. Compile blocks into a temporary bitmap of length cycleDuration
+    const bitmap = new Uint8Array(cycleDuration);
+    for (const b of blocks) {
+      let t = b.start;
+      const end = b.end;
+      while (t < end) {
+        bitmap[t % cycleDuration] = 1;
+        t++;
+      }
+    }
+
+    // 2. Extract exact transition points where state changes
+    for (let t = 0; t < cycleDuration; t++) {
+      const prev = bitmap[(t - 1 + cycleDuration) % cycleDuration];
+      const curr = bitmap[t];
+      if (curr !== prev) {
+        this.transitionMap.set(t, curr === 1);
+      }
+    }
+
+    // Set initial state based on tick 0
+    this.isActive = bitmap[0] === 1;
+  }
+
+  /**
+   * Inserters subscribe to receive direct state change callbacks.
+   */
+  public subscribe(callback: (isActive: boolean) => void) {
+    this.subscribers.push(callback);
+    // Push initial state immediately upon subscription
+    callback(this.isActive);
+  }
+
+  public tick(globalTick: number) {
+    const localTick = globalTick % this.cycleDuration;
+
+    // O(1) Check: Is this specific tick a transition boundary?
+    if (this.transitionMap.has(localTick)) {
+      const newState = this.transitionMap.get(localTick)!;
+      
+      // STATE CHANGE: Only trigger mutations when the boolean actually flips!
+      if (newState !== this.isActive) {
+        this.isActive = newState;
+        
+        for (const callback of this.subscribers) {
+          callback(this.isActive);
+        }
+      }
+    }
+  }
+}
+
+export class FactorioEngineOrchestrator {
+  public currentTick = 0;
+  
+  private rows = new Map<string, OptimizedClockRow>();
+  private belts: IContainer[] = [];
+  private inserters: InserterSimulator[] = [];
+  private machines: MachineSimulator[] = [];
+
+  public registerRow(rowId: string, blocks: { start: number; end: number }[], cycleDuration: number) {
+    const row = new OptimizedClockRow(rowId, blocks, cycleDuration);
+    this.rows.set(rowId, row);
+  }
+
+  public bindInserterToRow(inserter: InserterSimulator, rowId: string) {
+    const row = this.rows.get(rowId);
+    if (!row) throw new Error(`Row ${rowId} not found in orchestrator`);
+
+    // Pub/Sub binding: Inserter's isActive updates *only* when the row signals a state change
+    row.subscribe((active) => {
+      inserter.isActive = active;
+    });
+  }
+
+  // ... (keep belts, machines registers)
+
+  public registerInserter(inserter: InserterSimulator) {
+    this.inserters.push(inserter);
+  }
+
+  public tick() {
+    // --- PHASE 0: CLOCK & NETWORK STATE UPDATES ---
+    for (const row of this.rows.values()) {
+      row.tick(this.currentTick);
+    }
+
+    // --- PHASE 1: BELTS ---
+    for (const belt of this.belts) {
+      if (belt.tick) belt.tick(this.currentTick);
+    }
+
+    // --- PHASE 2: INSERTERS ---
+    const sleepingOrHoveringInserters: InserterSimulator[] = [];
+    const successfullyPickedInserters: InserterSimulator[] = [];
+
+    for (const inserter of this.inserters) {
+      const heldBefore = inserter.heldItems;
+      inserter.tick();
+      const pickedItemsThisTick = inserter.heldItems > heldBefore;
+      
+      if (pickedItemsThisTick) {
+        successfullyPickedInserters.push(inserter);
+      } else {
+        sleepingOrHoveringInserters.push(inserter);
+      }
+    }
+    this.inserters = [...sleepingOrHoveringInserters, ...successfullyPickedInserters];
+
+    // --- PHASE 3: MACHINES ---
+    for (const machine of this.machines) {
+      machine.tick(this.currentTick);
+    }
+
+    this.currentTick++;
+  }
+
+  public tickUntil(condition: () => boolean, maxTicks = 100000): boolean {
+    while (!condition() && this.currentTick < maxTicks) {
+      this.tick();
+    }
+    return this.currentTick < maxTicks;
+  }
+}
+// src/engine/simulator.test.ts
+
+  it('Optimized Clock: Pub/Sub row mapping controls inserter activation with zero scan overhead', () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+    
+    const machine = new MachineSimulator({ machineQualityLevel: 0 } as MachineSetup, basicRecipe, { 'iron-gear-wheel': 100 });
+    const source = new Chest('iron-plate');
+    const sink = new Chest();
+
+    // 1. Register Clock Rows (43-tick cycle)
+    orchestrator.registerRow('row_input', [{ start: 0, end: 8 }], 43);
+    orchestrator.registerRow('row_output', [{ start: 35, end: 43 }], 43);
+
+    // 2. Create Inserters
+    const inputInserter = new InserterSimulator(2, source, machine, 'iron-plate');
+    const outputInserter = new InserterSimulator(1, machine, sink, 'iron-gear-wheel');
+
+    // 3. Bind Inserters to Rows via Pub/Sub
+    orchestrator.bindInserterToRow(inputInserter, 'row_input');
+    orchestrator.bindInserterToRow(outputInserter, 'row_output');
+
+    orchestrator.registerInserter(inputInserter);
+    orchestrator.registerInserter(outputInserter);
+    orchestrator.registerMachine(machine);
+
+    // 4. Run exactly 1 cycle
+    orchestrator.tickUntil(() => false, 43);
+
+    // 5. Validate execution
+    expect(sink.receivedCounts['iron-gear-wheel']).toBe(1);
+    expect(inputInserter.swingCount).toBe(1);
+    expect(outputInserter.swingCount).toBe(1);
+  });
+// src/engine/simulator.test.ts
+
+  it('Unclocked Baseline: 3:2 Copper to GC stabilizes and executes perfect swing ratios', () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    const copperCableRecipe: Recipe = {
+      name: 'copper-cable', energy_required: 0.5,
+      ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
+      results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
+    } as Recipe;
+
+    const greenCircuitRecipe: Recipe = {
+      name: 'electronic-circuit', energy_required: 0.5, 
+      ingredients: [
+        { type: 'item', name: 'iron-plate', amount: 1 }, 
+        { type: 'item', name: 'copper-cable', amount: 3 }
+      ],
+      results: [{ type: 'item', name: 'electronic-circuit', amount: 1 }]
+    } as Recipe;
+
+    const assemblerSetup: MachineSetup = {
+      machine: { name: 'assembling-machine-2', crafting_speed: 1 } as Machine,
+      machineModules: [], beacons: [], 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 circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { 'electronic-circuit': 200 });
+    const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { 'electronic-circuit': 200 });
+
+    const sourceCopper = new Chest('copper-plate');
+    const sourceIron = new Chest('iron-plate');
+    const sinkGreenChips = new Chest();
+
+    // ALL inserters set to Stack Size 16
+    const inCop1 = new InserterSimulator(16, sourceCopper, cop1, 'copper-plate');
+    const inCop2 = new InserterSimulator(16, sourceCopper, cop2, 'copper-plate');
+    const inCop3 = new InserterSimulator(16, sourceCopper, cop3, 'copper-plate');
+    
+    const inIron1 = new InserterSimulator(16, sourceIron, circ1, 'iron-plate');
+    const inIron2 = new InserterSimulator(16, sourceIron, circ2, 'iron-plate');
+
+    // Direct insertions (Stack size 16)
+    const mid1 = new InserterSimulator(16, cop1, circ1, 'copper-cable');
+    const mid2a = new InserterSimulator(16, cop2, circ1, 'copper-cable');
+    const mid2b = new InserterSimulator(16, cop2, circ2, 'copper-cable');
+    const mid3 = new InserterSimulator(16, cop3, circ2, 'copper-cable');
+
+    const outCirc1 = new InserterSimulator(16, circ1, sinkGreenChips, 'electronic-circuit');
+    const outCirc2 = new InserterSimulator(16, circ2, sinkGreenChips, 'electronic-circuit');
+
+    const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
+    
+    allInserters.forEach(ins => orchestrator.registerInserter(ins));
+    [cop1, cop2, cop3, circ1, circ2].forEach(m => orchestrator.registerMachine(m));
+
+    // --- PHASE 1: STABILIZATION ---
+    // Run for 600 ticks (~10 seconds) to fill all buffers and start the cascades
+    orchestrator.tickUntil(() => false, 600);
+
+    // Reset all swing counters to 0 to prepare for the measurement window
+    allInserters.forEach(ins => ins.swingCount = 0);
+
+    // --- PHASE 2: MEASUREMENT WINDOW ---
+    // Run for exactly 1440 ticks. 
+    // Speed 1 machine = 30 ticks per craft. 1440 / 30 = 48 crafts perfectly.
+    // 48 crafts * 1 plate = 48 plates. 48 / 16 stack size = exactly 3 swings!
+    const targetTick = orchestrator.currentTick + 1440;
+    orchestrator.tickUntil(() => false, targetTick);
+
+    // Validate Copper Inputs (3 swings each = 48 plates per machine)
+    expect(inCop1.swingCount).toBe(3);
+    expect(inCop2.swingCount).toBe(3);
+    expect(inCop3.swingCount).toBe(3);
+
+    // Validate Iron Inputs (3 swings each = 48 plates per machine)
+    expect(inIron1.swingCount).toBe(3);
+    expect(inIron2.swingCount).toBe(3);
+
+    // Validate Outputs (3 swings each = 48 chips per machine)
+    expect(outCirc1.swingCount).toBe(3);
+    expect(outCirc2.swingCount).toBe(3);
+  });
+
+  // src/engine/simulator.test.ts
+
+  it('Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows', () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    const copperCableRecipe: Recipe = {
+      name: 'copper-cable', energy_required: 0.5,
+      ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
+      results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
+    } as Recipe;
+
+    const greenCircuitRecipe: Recipe = {
+      name: 'electronic-circuit', energy_required: 0.5, 
+      ingredients: [
+        { type: 'item', name: 'iron-plate', amount: 1 }, 
+        { type: 'item', name: 'copper-cable', amount: 3 }
+      ],
+      results: [{ type: 'item', name: 'electronic-circuit', amount: 1 }]
+    } as Recipe;
+
+    const assemblerSetup: MachineSetup = {
+      machine: { name: 'assembling-machine-2', crafting_speed: 1 } as Machine,
+      machineModules: [], beacons: [], 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 circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { 'electronic-circuit': 200 });
+    const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { 'electronic-circuit': 200 });
+
+    const sourceCopper = new Chest('copper-plate');
+    const sourceIron = new Chest('iron-plate');
+    const sinkGreenChips = new Chest();
+
+    // --- MATHEMATICAL CLOCK SETUP ---
+    // 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);
+    
+    // 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 }], 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);
+    
+    // Outputs (Green Chips): 16 items generated = 1 swing per cycle.
+    orchestrator.registerRow('row_outputs', [{ start: 460, end: 468 }], 480);
+
+    // Instantiate Inserters (Stack Size 16)
+    const inCop1 = new InserterSimulator(16, sourceCopper, cop1, 'copper-plate');
+    const inCop2 = new InserterSimulator(16, sourceCopper, cop2, 'copper-plate');
+    const inCop3 = new InserterSimulator(16, sourceCopper, cop3, 'copper-plate');
+    const inIron1 = new InserterSimulator(16, sourceIron, circ1, 'iron-plate');
+    const inIron2 = new InserterSimulator(16, sourceIron, circ2, 'iron-plate');
+
+    const mid1 = new InserterSimulator(16, cop1, circ1, 'copper-cable');
+    const mid2a = new InserterSimulator(16, cop2, circ1, 'copper-cable'); // Inner A
+    const mid2b = new InserterSimulator(16, cop2, circ2, 'copper-cable'); // Inner B
+    const mid3 = new InserterSimulator(16, cop3, circ2, 'copper-cable');
+
+    const outCirc1 = new InserterSimulator(16, circ1, sinkGreenChips, 'electronic-circuit');
+    const outCirc2 = new InserterSimulator(16, circ2, sinkGreenChips, 'electronic-circuit');
+
+    // 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');
+    [outCirc1, outCirc2].forEach(ins => orchestrator.bindInserterToRow(ins, 'row_outputs'));
+
+    const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
+    allInserters.forEach(ins => orchestrator.registerInserter(ins));
+    [cop1, cop2, cop3, circ1, circ2].forEach(m => orchestrator.registerMachine(m));
+
+    // --- PHASE 1: STABILIZATION ---
+    // Let the factory run for 3 full cycles (1440 ticks) so all machine buffers fill,
+    // the pipeline finishes, and steady-state clocked rhythm is established.
+    orchestrator.tickUntil(() => false, 1440);
+
+    // Reset counts for the true measurement
+    allInserters.forEach(ins => ins.swingCount = 0);
+    const startTarget = orchestrator.currentTick;
+
+    // --- PHASE 2: MEASUREMENT WINDOW ---
+    // Measure exactly 3 cycles (1440 ticks).
+    orchestrator.tickUntil(() => false, startTarget + 1440);
+
+    // --- VALIDATION ---
+    // Inputs: exactly 3 swings (1 per cycle)
+    expect(inCop1.swingCount).toBe(3);
+    expect(inCop2.swingCount).toBe(3);
+    expect(inIron1.swingCount).toBe(3);
+
+    // Mid Outer (cop1, cop3): exactly 6 swings (2 per cycle)
+    expect(mid1.swingCount).toBe(6);
+    expect(mid3.swingCount).toBe(6);
+
+    // Mid Inner (cop2): exactly 3 swings EACH (1 per cycle). 
+    // They did not fight because they were staggered by the clock!
+    expect(mid2a.swingCount).toBe(3);
+    expect(mid2b.swingCount).toBe(3);
+
+    // Outputs: exactly 3 swings (1 per cycle)
+    expect(outCirc1.swingCount).toBe(3);
+    expect(outCirc2.swingCount).toBe(3);
+
+    // Output yield matches maximum physical theoretical limit
+    // 3 swings * 16 items * 2 machines = 96
+    const outputAfterPhase1 = sinkGreenChips.receivedCounts['electronic-circuit'];
+    expect(outputAfterPhase1 - (sinkGreenChips.extractedCounts['electronic-circuit'] || 0)).toBeGreaterThanOrEqual(96);
+  });
+
+  // src/engine/simulator.test.ts
+
+  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);
+
+    // --- PHASE 1: Pull Copper ---
+    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.
+    filterInserter.updateDynamicFilters(['iron-plate']);
+
+    // Run for another 100 ticks
+    orchestrator.tickUntil(() => false, 200);
+
+    // It should now have extracted Iron as well!
+    expect(mixedBelt.extractedCounts['iron-plate']).toBeGreaterThan(0);
+    expect(sortingChest.receivedCounts['iron-plate']).toBeGreaterThan(0);
+  });
+
+  it('Advanced Filtering: Static multi-item filtering respects item locking', () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+    const mixedBelt = new Belt('iron-plate', 'copper-plate');
+    const sortingChest = new Chest();
+
+    // Configure static filters for BOTH Iron and Copper
+    const filterInserter = new FilterableInserterSimulator(16, mixedBelt, sortingChest, ['iron-plate', 'copper-plate']);
+    orchestrator.registerInserter(filterInserter);
+
+    // Tick 1: It evaluates canWakeUp(). Iron is first in the list, so it locks onto Iron!
+    orchestrator.tick();
+    
+    expect(filterInserter.state).toBe(InserterState.Picking);
+    expect(filterInserter.currentTargetItem).toBe('iron-plate'); // Locked!
+    expect(filterInserter.heldItems).toBe(4); // Belt pickup rate is 4
+
+    // Tick 2: It continues picking Iron. It does NOT mix Copper into the hand!
+    orchestrator.tick();
+    expect(filterInserter.heldItems).toBe(8);
+    expect(filterInserter.currentTargetItem).toBe('iron-plate');
+  });
+
+  // src/engine/simulator.test.ts
+
+  it('Advanced Filtering: Single multi-filter inserter autonomously feeds a Red Science machine', () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    // 1. 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
+    };
+
+    // 2. 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();
+
+    // 3. The Star of the Show: 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, ['copper-plate', 'iron-gear-wheel']);
+    const output = new InserterSimulator(4, machine, sinkChest, 'automation-science-pack');
+
+    orchestrator.registerInserter(smartInput);
+    orchestrator.registerInserter(output);
+    orchestrator.registerMachine(machine);
+
+    // 4. 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;
+    orchestrator.tickUntil(() => false, targetTick);
+
+    // --- VALIDATION ---
+    
+    // 1. Did the machine successfully craft? 
+    // Yes! 800 ticks is enough for 2 completed crafts.
+    expect(sinkChest.receivedCounts['automation-science-pack']).toBeGreaterThanOrEqual(2);
+
+    // 2. Did the filter inserter successfully switch items dynamically?
+    // 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);
+
+    // 3. Buffer Backpressure works!
+    // 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.
+    const copperPulled = mixedBelt.extractedCounts['copper-plate'];
+    const gearsPulled = mixedBelt.extractedCounts['iron-gear-wheel'];
+    
+    // They should be reasonably close to each other (within 1 hand size margin)
+    expect(Math.abs(copperPulled - gearsPulled)).toBeLessThanOrEqual(4);
+  });