Forráskód Böngészése

refine simulation

clovis 1 hónapja
szülő
commit
3c1faece44

+ 386 - 1
package-lock.json

@@ -40,7 +40,8 @@
         "tsconfig-paths": "^4.2.0",
         "typescript": "~5.9.3",
         "typescript-eslint": "^8.46.3",
-        "vite": "^8.2.0"
+        "vite": "^8.2.0",
+        "vitest": "^4.1.10"
       }
     },
     "node_modules/@babel/code-frame": {
@@ -1680,6 +1681,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@tsconfig/node10": {
       "version": "1.0.12",
       "dev": true,
@@ -1700,6 +1708,24 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/estree": {
       "version": "1.0.9",
       "dev": true,
@@ -2020,6 +2046,119 @@
         }
       }
     },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
+      "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
+      "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.10",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
+      "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
+      "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.10",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
+      "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
+      "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
+      "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.10",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.15.0",
       "dev": true,
@@ -2100,6 +2239,16 @@
       "dev": true,
       "license": "Python-2.0"
     },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/babel-plugin-macros": {
       "version": "3.1.0",
       "license": "MIT",
@@ -2215,6 +2364,16 @@
       ],
       "license": "CC-BY-4.0"
     },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "dev": true,
@@ -2398,6 +2557,13 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "node_modules/es-module-lexer": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
+      "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/escalade": {
       "version": "3.2.0",
       "dev": true,
@@ -2572,6 +2738,16 @@
         "node": ">=4.0"
       }
     },
+    "node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
     "node_modules/esutils": {
       "version": "2.0.3",
       "dev": true,
@@ -2580,6 +2756,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+      "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "dev": true,
@@ -3336,6 +3522,16 @@
         "yallist": "^3.0.2"
       }
     },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "dev": true,
@@ -3437,6 +3633,20 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/obug": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+      "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
     "node_modules/onetime": {
       "version": "7.0.0",
       "dev": true,
@@ -3585,6 +3795,13 @@
         "node": ">=8"
       }
     },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/picocolors": {
       "version": "1.1.1",
       "license": "ISC"
@@ -3937,6 +4154,13 @@
         "node": ">=8"
       }
     },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/signal-exit": {
       "version": "4.1.0",
       "dev": true,
@@ -3982,6 +4206,20 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/stdin-discarder": {
       "version": "0.2.2",
       "dev": true,
@@ -4066,6 +4304,23 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+      "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/tinyglobby": {
       "version": "0.2.17",
       "dev": true,
@@ -4108,6 +4363,16 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+      "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "dev": true,
@@ -4383,6 +4648,109 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/vitest": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
+      "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "4.1.10",
+        "@vitest/mocker": "4.1.10",
+        "@vitest/pretty-format": "4.1.10",
+        "@vitest/runner": "4.1.10",
+        "@vitest/snapshot": "4.1.10",
+        "@vitest/spy": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "4.1.10",
+        "@vitest/browser-preview": "4.1.10",
+        "@vitest/browser-webdriverio": "4.1.10",
+        "@vitest/coverage-istanbul": "4.1.10",
+        "@vitest/coverage-v8": "4.1.10",
+        "@vitest/ui": "4.1.10",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/vitest/node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "dev": true,
@@ -4397,6 +4765,23 @@
         "node": ">= 8"
       }
     },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "dev": true,

+ 3 - 2
src/assets/components/ClockWizard.tsx

@@ -180,7 +180,7 @@ export default function ClockWizard() {
               <div style={{ fontSize: "18px", color: "#7fc98a", fontWeight: "bold" }}>
                 {Math.round(throughputData.baseItemsPerMin)}{" "}
                 <span style={{ fontSize: "12px", color: "#999", fontWeight: "normal" }}>
-                  / min ({Math.round(throughputData.baseItemsPerMin / 6) / 10} / sec) 
+                  / min ({Math.round(throughputData.baseItemsPerMin / 6) / 10} / sec)
                 </span>
               </div>
             </div>
@@ -201,7 +201,8 @@ export default function ClockWizard() {
             </div>
             <div>
               <div style={{ fontSize: "11px", color: "#999" }}>Overload Limit</div>
-              <div style={{ fontSize: "14px", color: "#ffe6c0" }}>{stats.overloadMultiplier}x</div>{stats.singleCraftTicks}
+              <div style={{ fontSize: "14px", color: "#ffe6c0" }}>{stats.overloadMultiplier}x</div>
+              {stats.singleCraftTicks}
             </div>
           </div>
         )}

+ 0 - 96
src/assets/components/WizardTest.tsx

@@ -1,96 +0,0 @@
-import { useState, useMemo } from "react";
-import type { Recipe } from "../../../scripts/factorio-dump/helpers/recipes.helper";
-import { calculateOptimalBatch, generateAdvancedClock } from "../../engine/factorioEngine";
-
-const ADVANCED_CIRCUIT_RECIPE: Recipe = {
-  name: "advanced-circuit",
-  category: "electronics",
-  subgroup:"",
-  energy_required: 6, // Red chips take 6 seconds base
-  ingredients: [
-    { itemId: "electronic-circuit", amount: 2, type: "item" },
-    { itemId: "plastic-bar", amount: 2, type: "item" },
-    { itemId: "copper-cable", amount: 4, type: "item" },
-  ],
-  results: [
-    { type: "item", name: "advanced-circuit", amount: 1 }
-  ],
-};
-
-export default function WizardTest() {
-  const [productivityBonus, setProductivityBonus] = useState<number>(1.75); // +175%
-  const [actualSpeed, setActualSpeed] = useState<number>(10.0); // Fast endgame machine
-  const [stackSize, setStackSize] = useState<number>(16);
-
-  const result = useMemo(() => {
-    try {
-      const batch = calculateOptimalBatch(
-        ADVANCED_CIRCUIT_RECIPE,
-        productivityBonus,
-        actualSpeed,
-        stackSize
-      );
-      const clockData = generateAdvancedClock(batch, stackSize);
-      
-      return { batch, clockData };
-    } catch (err) {
-      return { error: (err as Error).message };
-    }
-  }, [productivityBonus, actualSpeed, stackSize]);
-
-  return (
-    <div style={{ padding: 20, fontFamily: "sans-serif", background: "#242324", color: "#ffe6c0" }}>
-      <h2>Factorio Clock Engine Sandbox</h2>
-      
-      <div style={{ display: "flex", gap: 20, marginBottom: 20 }}>
-        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
-          <label>Productivity Bonus (e.g. 1.75 for +175%)</label>
-          <input 
-            type="number" 
-            step="0.01" 
-            value={productivityBonus} 
-            onChange={(e) => setProductivityBonus(Number(e.target.value))} 
-            style={{ padding: 4 }}
-          />
-        </div>
-
-        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
-          <label>Actual Crafting Speed</label>
-          <input 
-            type="number" 
-            step="0.1" 
-            value={actualSpeed} 
-            onChange={(e) => setActualSpeed(Number(e.target.value))} 
-            style={{ padding: 4 }}
-          />
-        </div>
-
-        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
-          <label>Stack Size</label>
-          <input 
-            type="number" 
-            value={stackSize} 
-            onChange={(e) => setStackSize(Number(e.target.value))} 
-            style={{ padding: 4 }}
-          />
-        </div>
-      </div>
-
-      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20 }}>
-        <div>
-          <h3>1. Batch Math Result</h3>
-          <pre style={{ background: "#1a1a1a", padding: 10, borderRadius: 4, overflow: "auto" }}>
-            {JSON.stringify(result.batch, null, 2)}
-          </pre>
-        </div>
-        
-        <div>
-          <h3>2. Generated Timeline Blocks</h3>
-          <pre style={{ background: "#1a1a1a", padding: 10, borderRadius: 4, overflow: "auto" }}>
-            {JSON.stringify(result.clockData, null, 2)}
-          </pre>
-        </div>
-      </div>
-    </div>
-  );
-}

+ 413 - 91
src/engine/simulator.test.ts

@@ -1,159 +1,126 @@
-import { describe, it, expect } from 'vitest';
-import { 
-  MachineSimulator, 
-  InserterSimulator, 
-  Chest, 
-  SimulationOrchestrator, 
-  InserterState 
-} from './simulator';
-import type { MachineSetup } from './types';
-import type { Recipe } from '../../scripts/factorio-dump/helpers/recipes.helper';
-import type { Machine } from '../../scripts/factorio-dump/process-data.models';
-
-describe('Simulator Engine', () => {
+import { describe, it, expect, vi } from "vitest";
+import * as statsModule from "./stats";
+import {
+  MachineSimulator,
+  InserterSimulator,
+  Chest,
+  InserterState,
+  FactorioEngineOrchestrator,
+  Belt,
+} from "./simulator";
+import type { MachineSetup } from "./types";
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
+import type { Machine } from "../../scripts/factorio-dump/process-data.models";
 
+describe("Simulator Engine", () => {
   const mockMachineSetup: MachineSetup = {
     machine: { crafting_speed: 1 } as Machine,
     machineModules: [],
     beacons: [],
-    machineQualityLevel: 0
+    machineQualityLevel: 0,
   };
 
   const basicRecipe: Recipe = {
-    name: 'iron-gear-wheel',
+    name: "iron-gear-wheel",
     energy_required: 0.5, // 30 ticks
-    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 }],
   } as Recipe;
 
-  it('Machine Simulator: Crafts correctly over time and consumes inputs instantly', () => {
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { 'iron-gear-wheel': 100 });
-    
+  it("Machine Simulator: Crafts correctly over time and consumes inputs instantly", () => {
+    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+
     // Feed it enough for 2 crafts
-    machine.inputBuffer['iron-plate'] = 4;
-    
+    machine.inputBuffer["iron-plate"] = 4;
+
     machine.tick();
-    
+
     // Tick 1: Inputs consumed instantly, craft starts
     expect(machine.isCrafting).toBe(true);
-    expect(machine.inputBuffer['iron-plate']).toBe(2); 
-    expect(machine.outputBuffer['iron-gear-wheel']).toBe(0);
+    expect(machine.inputBuffer["iron-plate"]).toBe(2);
+    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(0);
 
     // Fast forward 28 ticks (29 total)
     for (let i = 0; i < 28; 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!
     machine.tick();
-    expect(machine.outputBuffer['iron-gear-wheel']).toBe(1);
-    
+    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(1);
+
     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);
+    expect(machine.inputBuffer["iron-plate"]).toBe(0);
   });
 
-  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 = {
       machine: { crafting_speed: 60 } as Machine, // 1 craft per tick!
       machineModules: [],
       beacons: [],
-      machineQualityLevel: 0
+      machineQualityLevel: 0,
     };
 
-    const machine = new MachineSimulator(fastSetup, basicRecipe, { 'iron-gear-wheel': 100 });
-    machine.inputBuffer['iron-plate'] = 20; // 10 crafts worth
+    const machine = new MachineSimulator(fastSetup, basicRecipe, { "iron-gear-wheel": 100 });
+    machine.inputBuffer["iron-plate"] = 20; // 10 crafts worth
 
     // 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.outputBuffer['iron-gear-wheel']).toBe(2);
+
+    expect(machine.inputBuffer["iron-plate"]).toBe(16); // 20 - 4
+    expect(machine.outputBuffer["iron-gear-wheel"]).toBe(2);
   });
 
-  it('Inserter Simulator: Executes exact tick phases for a Chest -> Machine transfer', () => {
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { 'iron-gear-wheel': 100 });
-    const sourceChest = new Chest('iron-plate');
-    const inserter = new InserterSimulator(16, sourceChest, machine, 'iron-plate');
+  it("Inserter Simulator: Executes exact tick phases for a Chest -> Machine transfer", () => {
+    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+    const sourceChest = new Chest("iron-plate");
+    const inserter = new InserterSimulator(16, sourceChest, machine, "iron-plate");
 
     expect(inserter.state).toBe(InserterState.Idle);
 
-    inserter.tick(); 
+    inserter.tick();
     // Tick 1: Wakes up, extracts instantly, and transitions to SwingingForward
     expect(inserter.state).toBe(InserterState.SwingingForward);
 
-    inserter.tick(); 
-    inserter.tick(); 
-    inserter.tick(); 
+    inserter.tick();
+    inserter.tick();
+    inserter.tick();
     // Ticks 2-4: Rotation (3 ticks) complete, transition to Dropping
     expect(inserter.state).toBe(InserterState.Dropping);
 
-    inserter.tick(); 
+    inserter.tick();
     // Tick 5: Drop complete (1 tick machine), contents deposited!
-    expect(machine.inputBuffer['iron-plate']).toBe(16);
+    expect(machine.inputBuffer["iron-plate"]).toBe(16);
     expect(inserter.state).toBe(InserterState.SwingingBack);
 
     inserter.tick();
     inserter.tick();
-    inserter.tick(); 
+    inserter.tick();
     // Ticks 6-8: Rotation back (3 ticks)
     expect(inserter.state).toBe(InserterState.Idle);
   });
 
-  it('End-to-End Simulation: Input -> Machine -> Output perfectly coordinated', () => {
-    const orchestrator = new SimulationOrchestrator();
-
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { 'iron-gear-wheel': 100 });
-    const sourceChest = new Chest('iron-plate');
-    const sinkChest = new Chest();
-
-    const inputInserter = new InserterSimulator(2, sourceChest, machine, 'iron-plate');
-    const outputInserter = new InserterSimulator(1, machine, sinkChest, 'iron-gear-wheel');
-
-    // Register in strict topological order mimicking Factorio's downstream update priority
-    orchestrator.register(inputInserter);
-    orchestrator.register(machine);
-    orchestrator.register(outputInserter);
-
-    // Run until the sink chest receives the final item
-    const success = orchestrator.tickUntil(() => (sinkChest.receivedCounts['iron-gear-wheel'] || 0) >= 1);
-    expect(success).toBe(true);
-
-    // Breakdown:
-    // T1: Input Inserter Picks.
-    // T2-4: Input Inserter Swings.
-    // T5: Input Inserter Drops. Machine gets input, starts craft (Progress=1).
-    // T6-33: Machine crafts...
-    // T34: Machine finishes 30th tick of craft. Item generated. Output Inserter wakes up, Picks.
-    // T35-37: Output Inserter Swings.
-    // T38: Output Inserter Drops into chest!
-    expect(orchestrator.currentTick).toBe(38);
-    
-    // Safely extracted from machine
-    expect(machine.outputBuffer['iron-gear-wheel']).toBe(0); 
-    expect(sinkChest.receivedCounts['iron-gear-wheel']).toBe(1);
-  });
-
-  it('Overload Limit validation: Inserter ignores target size if buffer is under limit', () => {
+  it("Overload Limit validation: Inserter ignores target size if buffer is under limit", () => {
     // We override the stats module or set up the config so overloadMultiplier is 3
     // For this test, we assume computeMachineStats returned an overloadMultiplier of 3.
-    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { 'iron-gear-wheel': 100 });
+    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
     machine.timings.overloadMultiplier = 3; // Forcing it to 3 for the test assertion
-    
-    const sourceChest = new Chest('iron-plate');
-    const inserter = new InserterSimulator(16, sourceChest, machine, 'iron-plate');
+
+    const sourceChest = new Chest("iron-plate");
+    const inserter = new InserterSimulator(16, sourceChest, machine, "iron-plate");
 
     expect(machine.timings.overloadMultiplier).toBe(3);
     // Overload limit = 2 (recipe amount) * 3 = 6 iron plates limit.
 
     // It should wake up and insert all 16 because the buffer is currently 0 (< 6)
     for (let i = 0; i < 5; i++) inserter.tick(); // 5 ticks for Chest->Machine swing
-    
-    expect(machine.inputBuffer['iron-plate']).toBe(16); // Overstuffed!
+
+    expect(machine.inputBuffer["iron-plate"]).toBe(16); // Overstuffed!
 
     // The machine instantly consumes 2, leaving 14.
     machine.tick();
-    expect(machine.inputBuffer['iron-plate']).toBe(14);
+    expect(machine.inputBuffer["iron-plate"]).toBe(14);
 
     // The inserter returns to idle
     for (let i = 0; i < 3; i++) inserter.tick(); // 3 ticks for rotation back
@@ -163,5 +130,360 @@ describe('Simulator Engine', () => {
     inserter.tick();
     expect(inserter.state).toBe(InserterState.Idle);
   });
-  
-});
+});
+describe("Factorio Strict Phase Orchestrator", () => {
+  const mockMachineSetup: MachineSetup = {
+    machine: { crafting_speed: 1 } as Machine,
+    machineModules: [],
+    beacons: [],
+    machineQualityLevel: 0,
+  };
+
+  const basicRecipe: Recipe = {
+    name: "iron-gear-wheel",
+    energy_required: 0.5, // 30 ticks
+    ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
+    results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
+  } as Recipe;
+
+  it("End-to-End: Proves the 1-tick delay caused by Inserters updating before Machines", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+    const sourceChest = new Chest("iron-plate");
+    const sinkChest = new Chest();
+
+    const inputInserter = new InserterSimulator(2, sourceChest, machine, "iron-plate");
+    const outputInserter = new InserterSimulator(1, machine, sinkChest, "iron-gear-wheel");
+
+    orchestrator.registerInserter(inputInserter);
+    orchestrator.registerInserter(outputInserter);
+    orchestrator.registerMachine(machine);
+
+    orchestrator.tickUntil(() => (sinkChest.receivedCounts["iron-gear-wheel"] || 0) >= 1);
+
+    // EXACT TIMELINE TRACE:
+    // T1: Input Picks.
+    // T2-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.
+
+    expect(orchestrator.currentTick).toBe(39);
+  });
+
+  it("Round-Robin: Two inserters compete for same output and share perfectly", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+
+    const sourceChest = new Chest("iron-plate");
+    const sinkChestA = new Chest();
+    const sinkChestB = new Chest();
+
+    const input = new InserterSimulator(16, sourceChest, machine, "iron-plate");
+
+    // Both output inserters have a hand size of 1
+    const outA = new InserterSimulator(1, machine, sinkChestA, "iron-gear-wheel");
+    const outB = new InserterSimulator(1, machine, sinkChestB, "iron-gear-wheel");
+
+    orchestrator.registerInserter(input);
+    orchestrator.registerInserter(outA); // A registered first!
+    orchestrator.registerInserter(outB);
+    orchestrator.registerMachine(machine);
+
+    // Run until 4 items are produced and extracted
+    orchestrator.tickUntil(
+      () =>
+        (sinkChestA.receivedCounts["iron-gear-wheel"] || 0) + (sinkChestB.receivedCounts["iron-gear-wheel"] || 0) === 4,
+    );
+
+    // 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!
+    expect(sinkChestA.receivedCounts["iron-gear-wheel"]).toBe(2);
+    expect(sinkChestB.receivedCounts["iron-gear-wheel"]).toBe(2);
+  });
+  it("Progressive Belt Pickup: Stack inserter extracts 4 items per tick until full", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    // Using the Belt helper from our previous setup
+    const sourceBelt = new Belt("iron-plate");
+    const sinkChest = new Chest();
+
+    // We set a weird hand size of 14.
+    // At 4 items per tick, this should take exactly 4 ticks (4, 4, 4, 2).
+    const inserter = new InserterSimulator(14, sourceBelt, sinkChest, "iron-plate");
+    orchestrator.registerInserter(inserter);
+
+    // Tick 1: Wakes up, picks 4.
+    orchestrator.tick();
+    expect(inserter.state).toBe(InserterState.Picking);
+    expect(inserter.heldItems).toBe(4);
+
+    // Tick 2: Picks 4.
+    orchestrator.tick();
+    expect(inserter.heldItems).toBe(8);
+
+    // Tick 3: Picks 4.
+    orchestrator.tick();
+    expect(inserter.heldItems).toBe(12);
+
+    // Tick 4: Picks remaining 2. Hand is full! Instantly transitions to Swinging.
+    orchestrator.tick();
+    expect(inserter.state).toBe(InserterState.SwingingForward);
+    expect(inserter.heldItems).toBe(14);
+  });
+  it("Round-Robin: Two inserters compete 4 stack for same output and share perfectly", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+    const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+
+    const sourceChest = new Chest("iron-plate");
+    const sinkChestA = new Chest();
+    const sinkChestB = new Chest();
+
+    const input = new InserterSimulator(16, sourceChest, machine, "iron-plate");
+
+    // Both output inserters have a hand size of 4
+    const outA = new InserterSimulator(4, machine, sinkChestA, "iron-gear-wheel");
+    const outB = new InserterSimulator(4, machine, sinkChestB, "iron-gear-wheel");
+
+    orchestrator.registerInserter(input);
+    orchestrator.registerInserter(outA); // A registered first!
+    orchestrator.registerInserter(outB);
+    orchestrator.registerMachine(machine);
+
+    // Run until 4 items are produced and extracted
+    orchestrator.tickUntil(() => false, 4 + 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!
+    expect(outA.heldItems).toBe(2);
+    expect(outB.heldItems).toBe(2);
+
+    orchestrator.tickUntil(() => sinkChestB.receivedCounts["iron-gear-wheel"] == 4);
+
+    expect(sinkChestB.receivedCounts["iron-gear-wheel"]).toBe(4);
+    expect(outA.heldItems).toBe(0);
+    expect(outB.heldItems).toBe(0);
+  });
+
+  it("True Round-Robin: Stack inserters alternate picking from a machine while hovering", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    // Custom recipe: Produces exactly 2 gears per craft
+    const twoGearRecipe: Recipe = {
+      name: "iron-gear-wheel",
+      energy_required: 0.5,
+      ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
+      results: [{ type: "item", name: "iron-gear-wheel", amount: 2 }], // Yields 2!
+    } as Recipe;
+
+    // Use quick machine (instant craft, 1 craft per tick)
+    const machine = new MachineSimulator(
+      { machine: { crafting_speed: 30 } as Machine, machineModules: [], beacons: [] } as MachineSetup,
+      twoGearRecipe,
+      {
+        "iron-gear-wheel": 100,
+      },
+    );
+
+    // Infinite input so the machine never stops
+    const sourceChest = new Chest("iron-plate");
+    const sinkChest = new Chest();
+
+    // Input inserter to keep machine stuffed
+    const input = new InserterSimulator(16, sourceChest, machine, "iron-plate");
+
+    // The two competing output inserters
+    const outA = new InserterSimulator(16, machine, sinkChest, "iron-gear-wheel");
+    const outB = new InserterSimulator(16, machine, sinkChest, "iron-gear-wheel");
+
+    // Register: outA gets priority first
+    orchestrator.registerInserter(input);
+    orchestrator.registerInserter(outA);
+    orchestrator.registerInserter(outB);
+    orchestrator.registerMachine(machine);
+
+    // --- Tick 1 ---
+    orchestrator.tick();
+    // Input grabs 16 plates.
+    // --- Tick 6 ---
+    // Fast forward to when input drops items into the machine
+    orchestrator.tickUntil(() => machine.inputBuffer["iron-plate"] > 0);
+    // Machine now has plates, and will craft 2 gears during its phase!
+
+    // --- Tick 7: The Competition Begins ---
+    orchestrator.tick();
+    // Inserter phase: outA wakes up, grabs the 2 gears. outB wakes up, but machine is empty.
+    expect(outA.state).toBe(InserterState.Picking);
+    expect(outB.state).toBe(InserterState.Idle);
+    expect(outA.heldItems).toBe(2);
+    expect(outB.heldItems).toBe(0);
+    // Machine phase: Crafts 2 more gears.
+
+    // --- Tick 8: Alternation! ---
+    orchestrator.tick();
+    // Inserter phase: outB has priority now! B grabs 2 gears. A gets nothing.
+    expect(outA.heldItems).toBe(2);
+    expect(outB.heldItems).toBe(2);
+    // Machine phase: Crafts 2 more gears.
+
+    // --- Tick 9 ---
+    orchestrator.tick();
+    // Inserter phase: outA has priority! A grabs 2 gears (now holding 4).
+    expect(outA.heldItems).toBe(4);
+    expect(outB.heldItems).toBe(2);
+
+    // --- Tick 10 ---
+    orchestrator.tick();
+    // B's turn again.
+    expect(outA.heldItems).toBe(4);
+    expect(outB.heldItems).toBe(4);
+
+    // Run until outA fills its hand (16 items) and transitions to SwingingForward
+    orchestrator.tickUntil(() => outA.state === InserterState.SwingingForward);
+
+    // Because they alternate 2 items at a time, when A hits 16 (and swings),
+    // B should be right behind it holding exactly 14!
+    expect(outA.heldItems).toBe(16);
+    expect(outB.heldItems).toBe(14);
+    expect(outB.state).toBe(InserterState.Picking);
+  });
+  it("Complex Recipe: Advanced Circuits with Productivity and multiple inputs", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    const advCircuitRecipe: Recipe = {
+      name: "advanced-circuit",
+      energy_required: 6, // 360 ticks
+      ingredients: [
+        { 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 }],
+    } as Recipe;
+
+    // A fast machine with +40% productivity
+    const advSetup: MachineSetup = {
+      machine: { crafting_speed: 6 } as Machine, // 60 ticks per craft
+      machineModules: [],
+      beacons: [],
+      machineQualityLevel: 0,
+    };
+
+    const machine = new MachineSimulator(advSetup, advCircuitRecipe, { "advanced-circuit": 200 });
+    // Manually inject productivity for test
+    machine.timings.productivityBonus = 0.4;
+    machine.timings.overloadMultiplier = 10;
+
+    const sourcePlastic = new Chest("plastic-bar");
+    const sourceCable = new Chest("copper-cable");
+    const sourceGreen = new Chest("electronic-circuit");
+    const sinkRed = new Chest();
+
+    orchestrator.registerInserter(new InserterSimulator(4, sourcePlastic, machine, "plastic-bar"));
+    orchestrator.registerInserter(new InserterSimulator(8, sourceCable, machine, "copper-cable"));
+    orchestrator.registerInserter(new InserterSimulator(4, sourceGreen, machine, "electronic-circuit"));
+    orchestrator.registerInserter(new InserterSimulator(16, machine, sinkRed, "advanced-circuit"));
+    orchestrator.registerMachine(machine);
+    // Run until 14 Red Chips are produced (10 base + 4 prod bonus)
+    const success = orchestrator.tickUntil(() => (sinkRed.receivedCounts["advanced-circuit"] || 0) >= 14);
+
+    expect(success).toBe(true);
+  });
+
+  it("Performance Stress Test: 1,000 machines for 10,000 ticks", () => {
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    const source = new Chest("iron-plate");
+    const sink = new Chest();
+
+    // Create a massive factory block
+    for (let i = 0; i < 1000; i++) {
+      const machine = new MachineSimulator(mockMachineSetup, basicRecipe, { "iron-gear-wheel": 100 });
+      orchestrator.registerMachine(machine);
+      orchestrator.registerInserter(new InserterSimulator(16, source, machine, "iron-plate"));
+      orchestrator.registerInserter(new InserterSimulator(16, machine, sink, "iron-gear-wheel"));
+    }
+
+    const start = performance.now();
+
+    // Run for 10,000 ticks (about 2.7 minutes of real Factorio game time)
+    orchestrator.tickUntil(() => false, 10000);
+
+    const end = performance.now();
+    const durationMs = end - start;
+
+    // Ensure the O(1) optimizations keep it fast!
+    // 1000 machines * 2000 inserters * 10,000 ticks = 30 million entity ticks.
+    // Should easily run in under 500ms in Node.js/Vitest.
+    expect(durationMs).toBeLessThan(1000);
+
+    // Verify they actually produced items
+    expect(sink.receivedCounts["iron-gear-wheel"]).toBeGreaterThan(100000);
+  });
+  it("Endgame Setup: Legendary EMP with Beacons perfectly matches batch metrics", () => {
+    // ISOLATED MOCK: Intercept computeMachineStats only for this test
+    const statsSpy = vi.spyOn(statsModule, "computeMachineStats").mockReturnValue({
+      actualCraftingSpeed: 84,
+      productivityBonus: 1.75,
+      singleCraftTicks: 4.2857142857142865,
+      craftsPerSecond: 14,
+      overloadMultiplier: 17,
+    });
+
+    const orchestrator = new FactorioEngineOrchestrator();
+
+    const advCircuitRecipe: Recipe = {
+      name: "advanced-circuit",
+      energy_required: 6,
+      ingredients: [
+        { 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 }],
+    } as Recipe;
+
+    const empSetup: MachineSetup = {
+      machine: { name: "electromagnetic-plant" } as Machine,
+      machineModules: [],
+      beacons: [],
+      machineQualityLevel: 5,
+    };
+
+    // The MachineSimulator will call the spy during its constructor and get our exact timings
+    const machine = new MachineSimulator(empSetup, advCircuitRecipe, { "advanced-circuit": 200 });
+
+    expect((machine as any).outputBlockLimits["advanced-circuit"]).toBe(17);
+
+    const sourcePlastic = new Chest("plastic-bar");
+    const sourceCable = new Chest("copper-cable");
+    const sourceGreen = new Chest("electronic-circuit");
+    const sinkRed = new Chest();
+
+    orchestrator.registerInserter(new InserterSimulator(16, sourcePlastic, machine, "plastic-bar"));
+    orchestrator.registerInserter(new InserterSimulator(16, sourceCable, machine, "copper-cable"));
+    orchestrator.registerInserter(new InserterSimulator(16, sourceGreen, machine, "electronic-circuit"));
+    orchestrator.registerInserter(new InserterSimulator(16, machine, sinkRed, "advanced-circuit"));
+    orchestrator.registerMachine(machine);
+
+    // Run until chest receives the batch size of 176
+    const success = orchestrator.tickUntil(() => (sinkRed.receivedCounts["advanced-circuit"] || 0) >= 176);
+    expect(success).toBe(true);
+
+    expect(sinkRed.receivedCounts["advanced-circuit"]).toBe(176);
+    expect(sourcePlastic.extractedCounts["plastic-bar"]).toBeGreaterThanOrEqual(128);
+    expect(sourceCable.extractedCounts["copper-cable"]).toBeGreaterThanOrEqual(256);
+    expect(sourceGreen.extractedCounts["electronic-circuit"]).toBeGreaterThanOrEqual(128);
+
+    // Timeline Duration: Base 275 + 5 + 5
+    expect(orchestrator.currentTick).toBe(285);
+
+    // CLEANUP: Restore the original function so other tests aren't affected
+    statsSpy.mockRestore();
+  });
+});

+ 92 - 29
src/engine/simulator.ts

@@ -25,13 +25,13 @@ export interface IContainer {
 
 export const INSERTER_TIMINGS = {
   ROTATION: 3,
-  PICKUP: {
-    [ContainerType.Chest]: 1,
-    [ContainerType.Machine]: 1,
-    [ContainerType.Belt]: 4,
-    [ContainerType.Splitter]: 4,
+  PICKUP_RATE: {
+    [ContainerType.Chest]: Infinity, // Grabs full hand instantly
+    [ContainerType.Machine]: Infinity, // Grabs full hand instantly
+    [ContainerType.Belt]: 4, // Grabs 4 items per tick
+    [ContainerType.Splitter]: 4, // Grabs 4 items per tick
   },
-  DROP: {
+  DROP_DELAY: {
     [ContainerType.Chest]: 1,
     [ContainerType.Machine]: 1,
     [ContainerType.Splitter]: 4,
@@ -77,8 +77,7 @@ export class MachineSimulator implements IContainer {
     for (const ing of ingredients) {
       if (ing.type === "item") {
         this.solidIngredients.push({ name: ing.name, amount: ing.amount });
-        this.overloadLimits[ing.name] =
-          ing.amount * this.timings.overloadMultiplier;
+        this.overloadLimits[ing.name] = ing.amount * this.timings.overloadMultiplier;
         this.inputBuffer[ing.name] = 0;
       }
     }
@@ -143,8 +142,7 @@ export class MachineSimulator implements IContainer {
 
   private isOutputBlocked(): boolean {
     for (const res of this.solidResults) {
-      if (this.outputBuffer[res.name] >= this.outputBlockLimits[res.name])
-        return true;
+      if (this.outputBuffer[res.name] >= this.outputBlockLimits[res.name]) return true;
     }
     return false;
   }
@@ -183,8 +181,10 @@ export class MachineSimulator implements IContainer {
 }
 export class Chest implements IContainer {
   public readonly type = ContainerType.Chest;
-  public receivedCounts: Record<string, number> = {}; // Tracks inserted items for testing
-  private readonly isSink: boolean; // Precomputed!
+  public receivedCounts: Record<string, number> = {};
+  public extractedCounts: Record<string, number> = {};
+
+  private readonly isSink: boolean;
   constructor(public providedItem?: string) {
     this.isSink = this.providedItem === undefined;
   }
@@ -199,7 +199,11 @@ export class Chest implements IContainer {
   }
 
   public extract(itemId: string, maxAmount: number): number {
-    return this.getAvailable(itemId) > 0 ? maxAmount : 0;
+    if (this.getAvailable(itemId)) {
+      this.extractedCounts[itemId] = (this.extractedCounts[itemId] || 0) + maxAmount;
+      return maxAmount;
+    }
+    return 0;
   }
 
   public insert(itemId: string, amount: number): void {
@@ -213,13 +217,10 @@ export class Belt implements IContainer {
   public readonly type = ContainerType.Belt;
   public receivedCounts: Record<string, number> = {};
 
-  private readonly isSink: boolean; // Precomputed!
+  private readonly isSink: boolean;
   private readonly providedItems = new Set<string>();
 
-  constructor(
-    providedItem1?: string,
-    providedItem2?: string,
-  ) {
+  constructor(providedItem1?: string, providedItem2?: string) {
     this.isSink = providedItem1 === undefined && providedItem2 === undefined;
 
     if (providedItem1) this.providedItems.add(providedItem1);
@@ -248,7 +249,8 @@ export class InserterSimulator {
   public state: InserterState = InserterState.Idle;
   public ticksInState = 0;
   public heldItems = 0;
-  private readonly pickupTicks: number;
+
+  private readonly pickupRate: number;
   private readonly dropTicks: number;
 
   constructor(
@@ -257,8 +259,8 @@ export class InserterSimulator {
     public destination: IContainer,
     public targetItemId: string,
   ) {
-    this.pickupTicks = INSERTER_TIMINGS.PICKUP[source.type];
-    this.dropTicks = INSERTER_TIMINGS.DROP[destination.type];
+    this.pickupRate = INSERTER_TIMINGS.PICKUP_RATE[source.type];
+    this.dropTicks = INSERTER_TIMINGS.DROP_DELAY[destination.type];
   }
 
   public tick() {
@@ -275,12 +277,17 @@ export class InserterSimulator {
 
     switch (this.state) {
       case InserterState.Picking:
-        // Reads the timing based on the INTERFACE type!
-        if (this.ticksInState >= this.pickupTicks) {
-          this.heldItems = this.source.extract(
-            this.targetItemId,
-            this.handSize,
-          );
+        // How many items do we still need to fill our hand?
+        const needed = this.handSize - this.heldItems;
+
+        // Grab either what we need, or the max we can pull per tick
+        const toPick = Math.min(needed, this.pickupRate);
+        const picked = this.source.extract(this.targetItemId, toPick);
+
+        this.heldItems += picked;
+
+        // Factorio Rule: The inserter doesn't rotate until its hand is FULL
+        if (this.heldItems >= this.handSize) {
           this.state = InserterState.SwingingForward;
           this.ticksInState = 0;
         }
@@ -318,8 +325,7 @@ export class InserterSimulator {
 
   private canWakeUp(): boolean {
     return (
-      this.source.getAvailable(this.targetItemId) > 0 &&
-      this.destination.canAccept(this.targetItemId, this.handSize)
+      this.source.getAvailable(this.targetItemId) > 0 && this.destination.canAccept(this.targetItemId, this.handSize)
     );
   }
 }
@@ -350,3 +356,60 @@ export class SimulationOrchestrator {
     return this.currentTick < maxTicks; // Returns true if condition met, false if timed out
   }
 }
+export class FactorioEngineOrchestrator {
+  public currentTick = 0;
+
+  // Entity Managers (Strict Execution Order!)
+  private belts: IContainer[] = [];
+  private inserters: InserterSimulator[] = [];
+  private machines: MachineSimulator[] = [];
+
+  public registerBelt(belt: IContainer) {
+    this.belts.push(belt);
+  }
+  public registerMachine(machine: MachineSimulator) {
+    this.machines.push(machine);
+  }
+
+  public registerInserter(inserter: InserterSimulator) {
+    this.inserters.push(inserter);
+  }
+
+  public tick() {
+    // 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) {
+        // It got items, so it yields priority for the next tick
+        successfullyPickedInserters.push(inserter);
+      } else {
+        // It got nothing (starved), so it maintains its priority at the front of the line
+        sleepingOrHoveringInserters.push(inserter);
+      }
+    }
+
+    this.inserters = [...sleepingOrHoveringInserters, ...successfullyPickedInserters];
+
+    // Assemblers
+    for (const machine of this.machines) {
+      machine.tick();
+    }
+
+    this.currentTick++;
+  }
+
+  public tickUntil(condition: () => boolean, maxTicks = 10000): boolean {
+    while (!condition() && this.currentTick < maxTicks) {
+      this.tick();
+    }
+    return this.currentTick < maxTicks;
+  }
+}

+ 0 - 2
src/main.tsx

@@ -6,7 +6,6 @@ import "./index.css";
 import ComponentTests from "./ComponentTests.tsx";
 import Layout from "./Layout.tsx";
 import ClockBuilder from "./ClockBuilder.tsx";
-import WizardTest from "./assets/components/WizardTest.tsx";
 import ClockWizard from "./assets/components/ClockWizard.tsx";
 
 const root = document.getElementById("root");
@@ -19,7 +18,6 @@ ReactDOM.createRoot(root as HTMLElement).render(
         <Route element={<Layout />}>
           <Route index element={<ClockBuilder />} />
           <Route path="tests" element={<ComponentTests />} />
-          <Route path="Wizar" element={<WizardTest />} />
           <Route path="InputConfigurator" element={<ClockWizard />} />
         </Route>
       </Routes>