|
@@ -1,22 +1,22 @@
|
|
|
-import { describe, it, expect } from 'vitest';
|
|
|
|
|
-import { calculateOptimalBatch } from './batch';
|
|
|
|
|
-import type { Recipe } from '../../scripts/factorio-dump/helpers/recipes.helper';
|
|
|
|
|
-import type { CalculatedTimings } from './types';
|
|
|
|
|
|
|
+import { describe, it, expect } from "vitest";
|
|
|
|
|
+import { calculateOptimalBatch } from "./batch";
|
|
|
|
|
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
|
|
|
|
|
+import type { CalculatedTimings } from "./model";
|
|
|
|
|
|
|
|
-describe('Batch Calculator', () => {
|
|
|
|
|
- it('calculates optimal batch for Copper Cable (1 plate -> 2 cables)', () => {
|
|
|
|
|
|
|
+describe("Batch Calculator", () => {
|
|
|
|
|
+ it("calculates optimal batch for Copper Cable (1 plate -> 2 cables)", () => {
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
energy_required: 0.5,
|
|
energy_required: 0.5,
|
|
|
- ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
|
|
|
|
|
- results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
|
|
|
|
|
|
|
+ ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
|
|
|
|
|
+ results: [{ type: "item", name: "copper-cable", amount: 2 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
actualCraftingSpeed: 1,
|
|
actualCraftingSpeed: 1,
|
|
|
productivityBonus: 0,
|
|
productivityBonus: 0,
|
|
|
singleCraftTicks: 30,
|
|
singleCraftTicks: 30,
|
|
|
craftsPerSecond: 2,
|
|
craftsPerSecond: 2,
|
|
|
- overloadMultiplier: 8
|
|
|
|
|
|
|
+ overloadMultiplier: 8,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// With a stack size of 16:
|
|
// With a stack size of 16:
|
|
@@ -27,48 +27,49 @@ describe('Batch Calculator', () => {
|
|
|
|
|
|
|
|
expect(batch.craftsPerCycle).toBe(16);
|
|
expect(batch.craftsPerCycle).toBe(16);
|
|
|
expect(batch.durationTicks).toBe(16 * 30); // 480 ticks
|
|
expect(batch.durationTicks).toBe(16 * 30); // 480 ticks
|
|
|
- expect(batch.inputs['copper-plate'].totalAmount).toBe(16); // 1 full stack
|
|
|
|
|
- expect(batch.outputs['copper-cable'].totalAmount).toBe(32); // 2 full stacks
|
|
|
|
|
|
|
+ expect(batch.inputs["copper-plate"].totalAmount).toBe(16); // 1 full stack
|
|
|
|
|
+ expect(batch.outputs["copper-cable"].totalAmount).toBe(32); // 2 full stacks
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('factors in productivity yields correctly', () => {
|
|
|
|
|
|
|
+ it("factors in productivity yields correctly", () => {
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
- ingredients: [{ type: 'item', name: 'iron-plate', amount: 2 }],
|
|
|
|
|
- results: [{ type: 'item', name: 'iron-gear-wheel', amount: 1 }]
|
|
|
|
|
|
|
+ ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
|
|
|
|
|
+ results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 50% productivity bonus means 1 craft yields 1.5 items
|
|
// 50% productivity bonus means 1 craft yields 1.5 items
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
actualCraftingSpeed: 1,
|
|
actualCraftingSpeed: 1,
|
|
|
- productivityBonus: 0.5,
|
|
|
|
|
|
|
+ productivityBonus: 0.5,
|
|
|
singleCraftTicks: 30,
|
|
singleCraftTicks: 30,
|
|
|
craftsPerSecond: 2,
|
|
craftsPerSecond: 2,
|
|
|
- overloadMultiplier: 8
|
|
|
|
|
|
|
+ overloadMultiplier: 8,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const batch = calculateOptimalBatch(mockRecipe as Recipe, timings);
|
|
const batch = calculateOptimalBatch(mockRecipe as Recipe, timings);
|
|
|
|
|
|
|
|
expect(batch.craftsPerCycle).toBe(32);
|
|
expect(batch.craftsPerCycle).toBe(32);
|
|
|
- expect(batch.inputs['iron-plate'].totalAmount).toBe(64); // 4 input stacks
|
|
|
|
|
- expect(batch.outputs['iron-gear-wheel'].totalAmount).toBe(48); // 3 output stacks
|
|
|
|
|
|
|
+ expect(batch.inputs["iron-plate"].totalAmount).toBe(64); // 4 input stacks
|
|
|
|
|
+ expect(batch.outputs["iron-gear-wheel"].totalAmount).toBe(48); // 3 output stacks
|
|
|
});
|
|
});
|
|
|
- it('ignores fluid ingredients and results when calculating LCM', () => {
|
|
|
|
|
|
|
+ it("ignores fluid ingredients and results when calculating LCM", () => {
|
|
|
// e.g. Battery recipe: 1 Iron Plate, 1 Copper Plate, 20 Sulfuric Acid (Fluid) -> 1 Battery
|
|
// e.g. Battery recipe: 1 Iron Plate, 1 Copper Plate, 20 Sulfuric Acid (Fluid) -> 1 Battery
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
energy_required: 4,
|
|
energy_required: 4,
|
|
|
ingredients: [
|
|
ingredients: [
|
|
|
- { type: 'item', name: 'iron-plate', amount: 1 },
|
|
|
|
|
- { type: 'item', name: 'copper-plate', amount: 1 },
|
|
|
|
|
- { type: 'fluid', name: 'sulfuric-acid', amount: 20 } // FLUID
|
|
|
|
|
|
|
+ { type: "item", name: "iron-plate", amount: 1 },
|
|
|
|
|
+ { type: "item", name: "copper-plate", amount: 1 },
|
|
|
|
|
+ { type: "fluid", name: "sulfuric-acid", amount: 20 }, // FLUID
|
|
|
],
|
|
],
|
|
|
- results: [
|
|
|
|
|
- { type: 'item', name: 'battery', amount: 1 }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ results: [{ type: "item", name: "battery", amount: 1 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
- actualCraftingSpeed: 1, productivityBonus: 0,
|
|
|
|
|
- singleCraftTicks: 240, craftsPerSecond: 0.25, overloadMultiplier: 8
|
|
|
|
|
|
|
+ actualCraftingSpeed: 1,
|
|
|
|
|
+ productivityBonus: 0,
|
|
|
|
|
+ singleCraftTicks: 240,
|
|
|
|
|
+ craftsPerSecond: 0.25,
|
|
|
|
|
+ overloadMultiplier: 8,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const batch = calculateOptimalBatch(mockRecipe as Recipe, timings);
|
|
const batch = calculateOptimalBatch(mockRecipe as Recipe, timings);
|
|
@@ -76,103 +77,109 @@ describe('Batch Calculator', () => {
|
|
|
// If fluids were not ignored, GCD/LCM math would break trying to divide 20 fluid by 16 items.
|
|
// If fluids were not ignored, GCD/LCM math would break trying to divide 20 fluid by 16 items.
|
|
|
// It should safely calculate LCM based ONLY on the item amounts (all are 1, so LCM = 16).
|
|
// It should safely calculate LCM based ONLY on the item amounts (all are 1, so LCM = 16).
|
|
|
expect(batch.craftsPerCycle).toBe(16);
|
|
expect(batch.craftsPerCycle).toBe(16);
|
|
|
- expect(batch.inputs['iron-plate']).toBeDefined();
|
|
|
|
|
- expect(batch.inputs['sulfuric-acid']).toBeUndefined(); // Fluids should not be scheduled for inserters
|
|
|
|
|
|
|
+ expect(batch.inputs["iron-plate"]).toBeDefined();
|
|
|
|
|
+ expect(batch.inputs["sulfuric-acid"]).toBeUndefined(); // Fluids should not be scheduled for inserters
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('calculates the output block limit correctly (bottlenecked by overload multiplier)', () => {
|
|
|
|
|
|
|
+ it("calculates the output block limit correctly (bottlenecked by overload multiplier)", () => {
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
- ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
|
|
|
|
|
- results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
|
|
|
|
|
|
|
+ ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
|
|
|
|
|
+ results: [{ type: "item", name: "copper-cable", amount: 2 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
- actualCraftingSpeed: 1, productivityBonus: 0, singleCraftTicks: 30, craftsPerSecond: 2,
|
|
|
|
|
- overloadMultiplier: 4 // Very slow machine, low overload limit
|
|
|
|
|
|
|
+ actualCraftingSpeed: 1,
|
|
|
|
|
+ productivityBonus: 0,
|
|
|
|
|
+ singleCraftTicks: 30,
|
|
|
|
|
+ craftsPerSecond: 2,
|
|
|
|
|
+ overloadMultiplier: 4, // Very slow machine, low overload limit
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// We pass a max stack size of 200 for the cable.
|
|
// We pass a max stack size of 200 for the cable.
|
|
|
- const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'copper-cable': 200 });
|
|
|
|
|
|
|
+ const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "copper-cable": 200 });
|
|
|
|
|
|
|
|
// Output block limit is Math.min(maxStack, overloadMultiplier * resultAmount)
|
|
// Output block limit is Math.min(maxStack, overloadMultiplier * resultAmount)
|
|
|
// Math.min(200, 4 * 2) = 8
|
|
// Math.min(200, 4 * 2) = 8
|
|
|
- expect(batch.outputs['copper-cable'].outputBlockLimit).toBe(8);
|
|
|
|
|
|
|
+ expect(batch.outputs["copper-cable"].outputBlockLimit).toBe(8);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('calculates the output block limit correctly (bottlenecked by max stack size)', () => {
|
|
|
|
|
|
|
+ it("calculates the output block limit correctly (bottlenecked by max stack size)", () => {
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
- ingredients: [{ type: 'item', name: 'iron-plate', amount: 1 }],
|
|
|
|
|
- results: [{ type: 'item', name: 'iron-gear-wheel', amount: 1 }]
|
|
|
|
|
|
|
+ ingredients: [{ type: "item", name: "iron-plate", amount: 1 }],
|
|
|
|
|
+ results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
- actualCraftingSpeed: 10, productivityBonus: 0, singleCraftTicks: 3, craftsPerSecond: 20,
|
|
|
|
|
- overloadMultiplier: 100 // Very fast machine, massive overload limit
|
|
|
|
|
|
|
+ actualCraftingSpeed: 10,
|
|
|
|
|
+ productivityBonus: 0,
|
|
|
|
|
+ singleCraftTicks: 3,
|
|
|
|
|
+ craftsPerSecond: 20,
|
|
|
|
|
+ overloadMultiplier: 100, // Very fast machine, massive overload limit
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// Iron gear wheels stack to 100 in Factorio
|
|
// Iron gear wheels stack to 100 in Factorio
|
|
|
- const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'iron-gear-wheel': 100 });
|
|
|
|
|
|
|
+ const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "iron-gear-wheel": 100 });
|
|
|
|
|
|
|
|
// Output block limit is Math.min(maxStack, overloadMultiplier * resultAmount)
|
|
// Output block limit is Math.min(maxStack, overloadMultiplier * resultAmount)
|
|
|
// Math.min(100, 100 * 1) = 100
|
|
// Math.min(100, 100 * 1) = 100
|
|
|
- expect(batch.outputs['iron-gear-wheel'].outputBlockLimit).toBe(100);
|
|
|
|
|
- expect(batch.outputs['iron-gear-wheel'].totalAmount).toBe(96);
|
|
|
|
|
|
|
+ expect(batch.outputs["iron-gear-wheel"].outputBlockLimit).toBe(100);
|
|
|
|
|
+ expect(batch.outputs["iron-gear-wheel"].totalAmount).toBe(96);
|
|
|
});
|
|
});
|
|
|
- it('handles complex multi-input recipes (Advanced Circuit) with productivity', () => {
|
|
|
|
|
|
|
+ it("handles complex multi-input recipes (Advanced Circuit) with productivity", () => {
|
|
|
// Advanced Circuit: 2 Plastic, 4 Copper Cable, 2 Green Chips -> 1 Red Chip
|
|
// Advanced Circuit: 2 Plastic, 4 Copper Cable, 2 Green Chips -> 1 Red Chip
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
ingredients: [
|
|
ingredients: [
|
|
|
- { type: 'item', name: 'plastic-bar', amount: 2 },
|
|
|
|
|
- { type: 'item', name: 'copper-cable', amount: 4 },
|
|
|
|
|
- { type: 'item', name: 'electronic-circuit', amount: 2 }
|
|
|
|
|
|
|
+ { type: "item", name: "plastic-bar", amount: 2 },
|
|
|
|
|
+ { type: "item", name: "copper-cable", amount: 4 },
|
|
|
|
|
+ { type: "item", name: "electronic-circuit", amount: 2 },
|
|
|
],
|
|
],
|
|
|
- results: [{ type: 'item', name: 'advanced-circuit', amount: 1 }]
|
|
|
|
|
|
|
+ results: [{ type: "item", name: "advanced-circuit", amount: 1 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
actualCraftingSpeed: 84,
|
|
actualCraftingSpeed: 84,
|
|
|
- productivityBonus: 1.75,
|
|
|
|
|
- singleCraftTicks: 4.2857142857142865,
|
|
|
|
|
|
|
+ productivityBonus: 1.75,
|
|
|
|
|
+ singleCraftTicks: 4.2857142857142865,
|
|
|
craftsPerSecond: 14,
|
|
craftsPerSecond: 14,
|
|
|
- overloadMultiplier: 17
|
|
|
|
|
|
|
+ overloadMultiplier: 17,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'advanced-circuit': 200 });
|
|
|
|
|
|
|
+ const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "advanced-circuit": 200 });
|
|
|
|
|
|
|
|
expect(batch.craftsPerCycle).toBe(64);
|
|
expect(batch.craftsPerCycle).toBe(64);
|
|
|
expect(batch.durationTicks).toBe(275);
|
|
expect(batch.durationTicks).toBe(275);
|
|
|
// 64 crafts * 2 per craft = 128 (8 inserter swings of 16)
|
|
// 64 crafts * 2 per craft = 128 (8 inserter swings of 16)
|
|
|
- expect(batch.inputs['plastic-bar'].totalAmount).toBe(128);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["plastic-bar"].totalAmount).toBe(128);
|
|
|
|
|
+
|
|
|
// 64 crafts * 4 per craft = 256 (16 inserter swings of 16)
|
|
// 64 crafts * 4 per craft = 256 (16 inserter swings of 16)
|
|
|
- expect(batch.inputs['copper-cable'].totalAmount).toBe(256);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["copper-cable"].totalAmount).toBe(256);
|
|
|
|
|
+
|
|
|
// 64 crafts * 2 per craft = 128 (8 inserter swings of 16)
|
|
// 64 crafts * 2 per craft = 128 (8 inserter swings of 16)
|
|
|
- expect(batch.inputs['electronic-circuit'].totalAmount).toBe(128);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["electronic-circuit"].totalAmount).toBe(128);
|
|
|
|
|
+
|
|
|
// 64 crafts * 2.75 yield = 176 (11 inserter swings of 16)
|
|
// 64 crafts * 2.75 yield = 176 (11 inserter swings of 16)
|
|
|
- expect(batch.outputs['advanced-circuit'].totalAmount).toBe(176);
|
|
|
|
|
- expect(batch.outputs['advanced-circuit'].outputBlockLimit).toBe(17);
|
|
|
|
|
|
|
+ expect(batch.outputs["advanced-circuit"].totalAmount).toBe(176);
|
|
|
|
|
+ expect(batch.outputs["advanced-circuit"].outputBlockLimit).toBe(17);
|
|
|
});
|
|
});
|
|
|
- it('handles prime-number fractions in extreme productivity scenarios (The Direct-Insertion Anchor)', () => {
|
|
|
|
|
|
|
+ it("handles prime-number fractions in extreme productivity scenarios (The Direct-Insertion Anchor)", () => {
|
|
|
// Productivity Module 1: 5 Advanced Circuit, 5 Electronic Circuit -> 1 Prod Module
|
|
// Productivity Module 1: 5 Advanced Circuit, 5 Electronic Circuit -> 1 Prod Module
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
ingredients: [
|
|
ingredients: [
|
|
|
- { type: 'item', name: 'advanced-circuit', amount: 5 },
|
|
|
|
|
- { type: 'item', name: 'electronic-circuit', amount: 5 }
|
|
|
|
|
|
|
+ { type: "item", name: "advanced-circuit", amount: 5 },
|
|
|
|
|
+ { type: "item", name: "electronic-circuit", amount: 5 },
|
|
|
],
|
|
],
|
|
|
- results: [{ type: 'item', name: 'productivity-module', amount: 1 }]
|
|
|
|
|
|
|
+ results: [{ type: "item", name: "productivity-module", amount: 1 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
actualCraftingSpeed: 5,
|
|
actualCraftingSpeed: 5,
|
|
|
productivityBonus: 1.15, // +115% yield (Yields 2.15 items per craft)
|
|
productivityBonus: 1.15, // +115% yield (Yields 2.15 items per craft)
|
|
|
- singleCraftTicks: 60,
|
|
|
|
|
|
|
+ singleCraftTicks: 60,
|
|
|
craftsPerSecond: 1,
|
|
craftsPerSecond: 1,
|
|
|
- overloadMultiplier: 4
|
|
|
|
|
|
|
+ overloadMultiplier: 4,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'productivity-module': 50 });
|
|
|
|
|
|
|
+ const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "productivity-module": 50 });
|
|
|
|
|
|
|
|
// --- The Math Breakdown ---
|
|
// --- The Math Breakdown ---
|
|
|
// 1. Productivity Yield:
|
|
// 1. Productivity Yield:
|
|
@@ -191,37 +198,37 @@ describe('Batch Calculator', () => {
|
|
|
// LCM(16, 320) = 320 crafts per cycle.
|
|
// LCM(16, 320) = 320 crafts per cycle.
|
|
|
|
|
|
|
|
expect(batch.craftsPerCycle).toBe(320);
|
|
expect(batch.craftsPerCycle).toBe(320);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Inputs: 320 crafts * 5 per craft = 1600.
|
|
// Inputs: 320 crafts * 5 per craft = 1600.
|
|
|
// 1600 / 16 = Exactly 100 perfect inserter swings per cycle!
|
|
// 1600 / 16 = Exactly 100 perfect inserter swings per cycle!
|
|
|
- expect(batch.inputs['advanced-circuit'].totalAmount).toBe(1600);
|
|
|
|
|
- expect(batch.inputs['electronic-circuit'].totalAmount).toBe(1600);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["advanced-circuit"].totalAmount).toBe(1600);
|
|
|
|
|
+ expect(batch.inputs["electronic-circuit"].totalAmount).toBe(1600);
|
|
|
|
|
+
|
|
|
// Outputs: 320 crafts * 2.15 yield = 688 items.
|
|
// Outputs: 320 crafts * 2.15 yield = 688 items.
|
|
|
// 688 / 16 = Exactly 43 perfect inserter swings per cycle!
|
|
// 688 / 16 = Exactly 43 perfect inserter swings per cycle!
|
|
|
- expect(batch.outputs['productivity-module'].totalAmount).toBe(688);
|
|
|
|
|
|
|
+ expect(batch.outputs["productivity-module"].totalAmount).toBe(688);
|
|
|
});
|
|
});
|
|
|
- it('safely caps batch scaling on high-volume output recipes to prevent buffer overflow', () => {
|
|
|
|
|
|
|
+ it("safely caps batch scaling on high-volume output recipes to prevent buffer overflow", () => {
|
|
|
// Copper Cable: 1 Copper Plate -> 2 Copper Cable
|
|
// Copper Cable: 1 Copper Plate -> 2 Copper Cable
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
- ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
|
|
|
|
|
- results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
|
|
|
|
|
|
|
+ ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
|
|
|
|
|
+ results: [{ type: "item", name: "copper-cable", amount: 2 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
actualCraftingSpeed: 10,
|
|
actualCraftingSpeed: 10,
|
|
|
productivityBonus: 2.5, // +250% (Total yield = 2 * 3.5 = 7 cables per craft)
|
|
productivityBonus: 2.5, // +250% (Total yield = 2 * 3.5 = 7 cables per craft)
|
|
|
- singleCraftTicks: 6,
|
|
|
|
|
|
|
+ singleCraftTicks: 6,
|
|
|
craftsPerSecond: 10,
|
|
craftsPerSecond: 10,
|
|
|
- overloadMultiplier: 50 // Machine can hold 50 * 1 plate = 50 plates buffer
|
|
|
|
|
|
|
+ overloadMultiplier: 50, // Machine can hold 50 * 1 plate = 50 plates buffer
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// We restrict the cable output buffer to max stack (200)
|
|
// We restrict the cable output buffer to max stack (200)
|
|
|
- const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'copper-cable': 200 });
|
|
|
|
|
|
|
+ const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "copper-cable": 200 });
|
|
|
|
|
|
|
|
// --- The Math Breakdown ---
|
|
// --- The Math Breakdown ---
|
|
|
// Output Yield = 7 cables per craft.
|
|
// Output Yield = 7 cables per craft.
|
|
|
- // Base LCM:
|
|
|
|
|
|
|
+ // Base LCM:
|
|
|
// Inputs (1): 16 / gcd(1, 16) = 16 crafts.
|
|
// Inputs (1): 16 / gcd(1, 16) = 16 crafts.
|
|
|
// Outputs (yield 7): Denominator (16) / gcd(7, 16) = 16 crafts.
|
|
// Outputs (yield 7): Denominator (16) / gcd(7, 16) = 16 crafts.
|
|
|
// baseOptimalN = 16 crafts.
|
|
// baseOptimalN = 16 crafts.
|
|
@@ -233,33 +240,33 @@ describe('Batch Calculator', () => {
|
|
|
//
|
|
//
|
|
|
// WAIT! maxSafeCrafts (14) is LESS than baseOptimalN (16).
|
|
// WAIT! maxSafeCrafts (14) is LESS than baseOptimalN (16).
|
|
|
// The engine CANNOT safely scale down below the base LCM, or else we lose perfect 16-stack swings.
|
|
// The engine CANNOT safely scale down below the base LCM, or else we lose perfect 16-stack swings.
|
|
|
- // It should fallback to baseOptimalN (16) and rely on the Timeline generator's
|
|
|
|
|
|
|
+ // It should fallback to baseOptimalN (16) and rely on the Timeline generator's
|
|
|
// drip-feed logic to keep the machine from jamming.
|
|
// drip-feed logic to keep the machine from jamming.
|
|
|
|
|
|
|
|
expect(batch.craftsPerCycle).toBe(16); // Remains at the absolute mathematical minimum
|
|
expect(batch.craftsPerCycle).toBe(16); // Remains at the absolute mathematical minimum
|
|
|
- expect(batch.inputs['copper-plate'].totalAmount).toBe(16);
|
|
|
|
|
- expect(batch.outputs['copper-cable'].totalAmount).toBe(112); // 16 * 7
|
|
|
|
|
|
|
+ expect(batch.inputs["copper-plate"].totalAmount).toBe(16);
|
|
|
|
|
+ expect(batch.outputs["copper-cable"].totalAmount).toBe(112); // 16 * 7
|
|
|
});
|
|
});
|
|
|
- it('handles Legendary Max-Out (+100% Prod) with a highly restrictive overload limit', () => {
|
|
|
|
|
|
|
+ it("handles Legendary Max-Out (+100% Prod) with a highly restrictive overload limit", () => {
|
|
|
// Chemical Science Pack: 2 Engine, 3 Adv Circuit, 1 Sulfur -> 2 Chemical Science Packs
|
|
// Chemical Science Pack: 2 Engine, 3 Adv Circuit, 1 Sulfur -> 2 Chemical Science Packs
|
|
|
const mockRecipe: Partial<Recipe> = {
|
|
const mockRecipe: Partial<Recipe> = {
|
|
|
ingredients: [
|
|
ingredients: [
|
|
|
- { type: 'item', name: 'engine-unit', amount: 2 },
|
|
|
|
|
- { type: 'item', name: 'advanced-circuit', amount: 3 },
|
|
|
|
|
- { type: 'item', name: 'sulfur', amount: 1 }
|
|
|
|
|
|
|
+ { type: "item", name: "engine-unit", amount: 2 },
|
|
|
|
|
+ { type: "item", name: "advanced-circuit", amount: 3 },
|
|
|
|
|
+ { type: "item", name: "sulfur", amount: 1 },
|
|
|
],
|
|
],
|
|
|
- results: [{ type: 'item', name: 'chemical-science-pack', amount: 2 }]
|
|
|
|
|
|
|
+ results: [{ type: "item", name: "chemical-science-pack", amount: 2 }],
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const timings: CalculatedTimings = {
|
|
const timings: CalculatedTimings = {
|
|
|
actualCraftingSpeed: 10,
|
|
actualCraftingSpeed: 10,
|
|
|
productivityBonus: 1.0, // +100% (4 Legendary Prod 3 Modules)
|
|
productivityBonus: 1.0, // +100% (4 Legendary Prod 3 Modules)
|
|
|
- singleCraftTicks: 14.4,
|
|
|
|
|
|
|
+ singleCraftTicks: 14.4,
|
|
|
craftsPerSecond: 4.16,
|
|
craftsPerSecond: 4.16,
|
|
|
- overloadMultiplier: 3 // Extremely restrictive! Machine will stall almost instantly.
|
|
|
|
|
|
|
+ overloadMultiplier: 3, // Extremely restrictive! Machine will stall almost instantly.
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'chemical-science-pack': 200 });
|
|
|
|
|
|
|
+ const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "chemical-science-pack": 200 });
|
|
|
|
|
|
|
|
// --- The Math Breakdown ---
|
|
// --- The Math Breakdown ---
|
|
|
// 1. Productivity: Yield is 2 * 2.0 = 4 items per craft. (prod fraction = 2/1)
|
|
// 1. Productivity: Yield is 2 * 2.0 = 4 items per craft. (prod fraction = 2/1)
|
|
@@ -276,28 +283,28 @@ describe('Batch Calculator', () => {
|
|
|
//
|
|
//
|
|
|
// 4. Base LCM = LCM(16, 4) = 16 crafts.
|
|
// 4. Base LCM = LCM(16, 4) = 16 crafts.
|
|
|
//
|
|
//
|
|
|
- // 5. Buffer Limits (maxSafeCrafts):
|
|
|
|
|
|
|
+ // 5. Buffer Limits (maxSafeCrafts):
|
|
|
// Output limit = min(200, 3 (overload) * 2 (amount)) = 6 items max in output buffer!
|
|
// Output limit = min(200, 3 (overload) * 2 (amount)) = 6 items max in output buffer!
|
|
|
- // Yield per craft is 4.
|
|
|
|
|
|
|
+ // Yield per craft is 4.
|
|
|
// Max safe crafts = Math.floor(6 / 4) = 1 craft!
|
|
// Max safe crafts = Math.floor(6 / 4) = 1 craft!
|
|
|
//
|
|
//
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
expect(batch.craftsPerCycle).toBe(16);
|
|
expect(batch.craftsPerCycle).toBe(16);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Inputs:
|
|
// Inputs:
|
|
|
// Engine: 16 * 2 = 32 (Exactly 2 swings of 16)
|
|
// Engine: 16 * 2 = 32 (Exactly 2 swings of 16)
|
|
|
- expect(batch.inputs['engine-unit'].totalAmount).toBe(32);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["engine-unit"].totalAmount).toBe(32);
|
|
|
|
|
+
|
|
|
// Adv Circuit: 16 * 3 = 48 (Exactly 3 swings of 16)
|
|
// Adv Circuit: 16 * 3 = 48 (Exactly 3 swings of 16)
|
|
|
- expect(batch.inputs['advanced-circuit'].totalAmount).toBe(48);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["advanced-circuit"].totalAmount).toBe(48);
|
|
|
|
|
+
|
|
|
// Sulfur: 16 * 1 = 16 (Exactly 1 swing of 16)
|
|
// Sulfur: 16 * 1 = 16 (Exactly 1 swing of 16)
|
|
|
- expect(batch.inputs['sulfur'].totalAmount).toBe(16);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.inputs["sulfur"].totalAmount).toBe(16);
|
|
|
|
|
+
|
|
|
// Output: 16 * 4 yield = 64 items (Exactly 4 swings of 16)
|
|
// Output: 16 * 4 yield = 64 items (Exactly 4 swings of 16)
|
|
|
- expect(batch.outputs['chemical-science-pack'].totalAmount).toBe(64);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ expect(batch.outputs["chemical-science-pack"].totalAmount).toBe(64);
|
|
|
|
|
+
|
|
|
// Validates that the engine properly identified the output bottleneck
|
|
// Validates that the engine properly identified the output bottleneck
|
|
|
- expect(batch.outputs['chemical-science-pack'].outputBlockLimit).toBe(6);
|
|
|
|
|
|
|
+ expect(batch.outputs["chemical-science-pack"].outputBlockLimit).toBe(6);
|
|
|
});
|
|
});
|
|
|
-});
|
|
|
|
|
|
|
+});
|