diff --git a/engine/src/game/Game.ts b/engine/src/game/Game.ts index 56e8604..34fc9b3 100644 --- a/engine/src/game/Game.ts +++ b/engine/src/game/Game.ts @@ -14,7 +14,10 @@ export class Game { rules: GameRules, ) { this.rules = rules; + this.reset(); + } + public reset() { this.initResources(); this.initGenerators(); } diff --git a/engine/src/game/ResourceGenerator.ts b/engine/src/game/ResourceGenerator.ts index 6ed0caf..aaee905 100644 --- a/engine/src/game/ResourceGenerator.ts +++ b/engine/src/game/ResourceGenerator.ts @@ -112,7 +112,7 @@ export class ResourceGenerator { // } public getGain(level?: number): ResourceSet { - return this.rule.generationGain(level ?? this.level, this.game); + return this.rule.generationGain(level ?? this.level, this.game).mul(this.getPeriod(level)); } public snapshot(): ResourceGeneratorSnapshot { diff --git a/engine/src/game/ResourceSet.ts b/engine/src/game/ResourceSet.ts index 591e707..a548ef0 100644 --- a/engine/src/game/ResourceSet.ts +++ b/engine/src/game/ResourceSet.ts @@ -79,8 +79,14 @@ export class ResourceSet { return inst; } + public div(factor: number): ResourceSet { + const inst = new ResourceSet(); + ResourceSet.transform(this.keys(), inst, (res) => this.get(res).divide(factor)); + return inst; + } + public toString(): string { - return Array.from(this.items).map(([k, v]) => `${v} ${k}`).join(', '); + return Array.from(this.items).map(([k, v]) => `${formatNumber(v)} ${k}`).join(', '); } public toObject(): PartialResources { diff --git a/engine/src/index.ts b/engine/src/index.ts index 3a9994f..244b0dd 100644 --- a/engine/src/index.ts +++ b/engine/src/index.ts @@ -5,8 +5,8 @@ export * from './rules'; export * from './game'; export * from './actions'; -import { rules } from './rulesets/rules'; +import { rules, type GameRulesOptions } from './rulesets/rules'; -export function makeGame(): Game { - return new Game(rules) +export function makeGame(options: GameRulesOptions): Game { + return new Game(rules(options)) }; diff --git a/engine/src/rulesets/rules copy 2.ts b/engine/src/rulesets/rules copy 2.ts new file mode 100644 index 0000000..fa80f82 --- /dev/null +++ b/engine/src/rulesets/rules copy 2.ts @@ -0,0 +1,65 @@ +import Decimal from "break_eternity.js"; +import type { GameRules } from "../rules/GameRules"; +import { simpleResGen, resourceGen } from './utils'; +import { ResourceSet } from "../game"; + +export const rules: GameRules = { + startResources: { prestigeMul: new Decimal(1) }, + generators: [ + { + name: 'white', + startingLevel: 1, + resetProgressOnUpgrade: true, + manualGeneration: true, + period: (level) => level ? 3 : 0, + isVisible: () => true, + upgradePrice: () => ResourceSet.zero, + isUpgradable: () => false, + generationGain: (level, game) => resourceGen({ RED: { offset: 14, power: 2, factor: 1 } }, level), + onGenerate: (level, game, gain) => game.resources.add(gain), + }, + // simpleResGen({ + // name: 'red', + // startingLevel: 1, + // period: (level) => level === 0 ? 0 : Math.pow(0.95, level - 1), + // upgradePrice: { RED: { offset: 3, power: 2.1, factor: 10 } }, + // generatesResources: { RED: { offset: 3, power: 1.5, factor: 1 } }, + // }), + // ...['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'violet'] + ...['red', 'orange', 'yellow'] + .map((color, idx) => simpleResGen({ + name: color, + startingLevel: idx === 0 ? 1 : 0, + period: (level) => level === 0 ? 0 : Math.pow(0.95, level) + 0.05, + generatesResources: { RED: { offset: 2 * 15 ** idx, power: 1, factor: 1 * 15 ** idx } }, + // visibilityPrice: { RED: { offset: 5 * 10 ** (idx + 1) } }, + upgradePrice: { RED: { offset: 2 * 20 ** idx, power: 2.6, factor: 3 * 15 ** idx } } + })), + // { + // name: 'prestige', + // startingLevel: 0, + // period: () => 0, + // // generation: resourceGen({ prestigeMul: { offset: 0, power: 0.2, factor: 1 } }), + // openPrice: (level) => ({ RED: new Decimal(50) }), + // upgradePrice: () => ({}), + // visibilityPrice: () => ({}), + // manualProgressReset: true, + // resetProgressOnUpgrade: true, + // generation: (level, game, dryRun) => { + // if (!dryRun) { + // console.log('prestige ' + level); + // game.generators.filter((gen) => gen.rule.name !== 'prestige').forEach((gen) => gen.reset()); + // ResourceSet.transform( + // game.resources.keys().filter((res) => res != 'prestigeMul'), + // game.resources, + // () => Decimal.dZero, + // ); + // } + + // return resourceGen({ prestigeMul: { offset: 0, power: 1, factor: 1 } })(level, game); + + // // game.resources.clear(); + // } + // } + ], +} \ No newline at end of file diff --git a/engine/src/rulesets/rules.ts b/engine/src/rulesets/rules.ts index 943bfd5..e974873 100644 --- a/engine/src/rulesets/rules.ts +++ b/engine/src/rulesets/rules.ts @@ -1,37 +1,52 @@ import Decimal from "break_eternity.js"; import type { GameRules } from "../rules/GameRules"; -import { simpleResGen, resourceGen } from './utils'; +import { resourceGen, classicResGen } from './utils'; import { ResourceSet } from "../game"; -export const rules: GameRules = { - startResources: { prestigeMul: new Decimal(1) }, +export type GameRulesOptions = { + startingResources: number, + upgradeBasePrice: number, + generationBasePrice: number, + upgradeFactor: number, + generationPower: number, +} + +export const defaultGameRulesOptions: GameRulesOptions = { + startingResources: 100, + generationBasePrice: 0.2, + generationPower: 0.3, + upgradeBasePrice: 100, + upgradeFactor: 1.01, +} + +export const rules: (options: GameRulesOptions) => GameRules = (options) => ({ + startResources: { RED: new Decimal(options.startingResources), prestigeMul: new Decimal(1) }, generators: [ - { - name: 'white', - startingLevel: 1, - resetProgressOnUpgrade: true, - manualGeneration: true, - period: (level) => level ? 3 : 0, - isVisible: () => true, - upgradePrice: () => ResourceSet.zero, - isUpgradable: () => false, - generationGain: (level, game) => resourceGen({ RED: { offset: 14, power: 2, factor: 1 } }, level), - onGenerate: (level, game, gain) => game.resources.add(gain), - }, - simpleResGen({ - name: 'red', - startingLevel: 1, - period: 1, - upgradePrice: { RED: { offset: 3, power: 2.1, factor: 10 } }, - generatesResources: { RED: { offset: 3, power: 1.5, factor: 1 } }, - }), - ...['orange', 'yellow', 'green', 'cyan', 'blue', 'violet'] - .map((color, idx) => simpleResGen({ + // { + // name: 'white', + // startingLevel: 1, + // resetProgressOnUpgrade: true, + // manualGeneration: true, + // period: (level) => level ? 3 : 0, + // isVisible: () => true, + // upgradePrice: () => ResourceSet.zero, + // isUpgradable: () => false, + // generationGain: (level, game) => resourceGen({ RED: { offset: 14, power: 2, factor: 1 } }, level), + // onGenerate: (level, game, gain) => game.resources.add(gain), + // }, + + // simpleResGen({ + // name: 'red', + // startingLevel: 1, + // period: (level) => level === 0 ? 0 : Math.pow(0.95, level - 1), + // upgradePrice: { RED: { offset: 3, power: 2.1, factor: 10 } }, + // generatesResources: { RED: { offset: 3, power: 1.5, factor: 1 } }, + // }), + // ...['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'violet'] + ...['red'] + .map((color, idx) => classicResGen(options, { name: color, - period: idx + 2, - generatesResources: { RED: { power: 1.5, factor: (idx + 2) ** 2 } }, - // visibilityPrice: { RED: { offset: 5 * 10 ** (idx + 1) } }, - upgradePrice: { RED: { offset: 10 ** (idx + 2), power: 2.1, factor: 10 * 5 ** (idx + 1) } } + startingLevel: 1, })), // { // name: 'prestige', @@ -60,4 +75,4 @@ export const rules: GameRules = { // } // } ], -} \ No newline at end of file +}); diff --git a/engine/src/rulesets/utils.ts b/engine/src/rulesets/utils.ts index e78b6eb..5047549 100644 --- a/engine/src/rulesets/utils.ts +++ b/engine/src/rulesets/utils.ts @@ -1,7 +1,7 @@ import Decimal from "break_eternity.js"; -import type { PartialResources, Resources } from "../types"; import type { ResourceGeneratorRule } from "../rules/ResourceGeneratorRule"; import { ResourceSet, type Game } from "../game"; +import type { GameRulesOptions } from "./rules"; type ResourceGenParams = { offset?: number, @@ -12,7 +12,7 @@ type ResourceGenParams = { type SimpleResGenParams = { name: string, startingLevel?: number, - period: number, + period: (level: number, game: Game) => number, visibilityPrice?: Partial>, // openPrice: Partial>, upgradePrice: Partial>, @@ -65,11 +65,31 @@ export function simpleResGen(params: SimpleResGenParams): ResourceGeneratorRule startingLevel: params.startingLevel ?? 0, resetProgressOnUpgrade: true, manualGeneration: false, - period: (level) => level ? params.period : 0, + period: params.period, isVisible: (level, game) => game.resources.gte(resourceGen(params.visibilityPrice, level)), upgradePrice: (level, game) => resourceGen(params.upgradePrice, level), isUpgradable: (level, game, upgradePrice) => game.resources.gte(upgradePrice), generationGain: (level, game) => resourceGen(params.generatesResources, level), onGenerate: (level, game, gain) => game.resources.add(gain), } +} + +type ClassicResGenParams = { + name: string, + startingLevel: number, +} + +export function classicResGen(options: GameRulesOptions, params: ClassicResGenParams): ResourceGeneratorRule { + return { + name: params.name, + startingLevel: params.startingLevel ?? 0, + resetProgressOnUpgrade: true, + manualGeneration: false, + period: () => 1, + isVisible: (level, game) => true, + upgradePrice: (level, game) => ResourceSet.from({ RED: new Decimal(options.upgradeBasePrice * options.upgradeFactor ** level) }), + isUpgradable: (level, game, upgradePrice) => game.resources.gte(upgradePrice), + generationGain: (level, game) => ResourceSet.from({ RED: new Decimal(options.generationBasePrice * level ** options.generationPower) }), + onGenerate: (level, game, gain) => game.resources.add(gain), + }; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dbc43ad..d27a689 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "workspaces": [ "engine", "web", - "simulator" + "simulator", + "simulator-web" ], "dependencies": { "break_eternity.js": "^2.1.3" @@ -1002,6 +1003,10 @@ "resolved": "simulator", "link": true }, + "node_modules/@idle-economy/simulator-web": { + "resolved": "simulator-web", + "link": true + }, "node_modules/@idle-economy/web": { "resolved": "web", "link": true @@ -1056,6 +1061,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -1093,6 +1104,289 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.17.1.tgz", + "integrity": "sha512-+VuZyMYYaap5uDAU1xDU3Kul0FekLqpBS8kI5JozlWfYQKnc/HsZg2gHPkQrj0SC9lt74WMNCfOzZZJlYXSdEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.17.1.tgz", + "integrity": "sha512-YlDDTjvOEKhom/cRSVsXsMVeXVIAM9PJ/x2mfe08rfuS0iIEfJd8PngKbEIhG72WPxleUa+vkEZj9ncmC14z3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.17.1.tgz", + "integrity": "sha512-HOYYLSY4JDk14YkXaz/ApgJYhgDP4KsG8EZpgpOxdszGW9HmIMMY/vXqVKYW74dSH+GQkIXYxBrEh3nv+XODVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.17.1.tgz", + "integrity": "sha512-JHPJbsa5HvPq2/RIdtGlqfaG9zV2WmgvHrKTYmlW0L5esqtKCBuetFudXTBzkNcyD69kSZLzH92AzTr6vFHMFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.17.1.tgz", + "integrity": "sha512-UD1FRC8j8xZstFXYsXwQkNmmg7vUbee006IqxokwDUUA+xEgKZDpLhBEiVKM08Urb+bn7Q0gn6M1pyNR0ng5mg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.17.1.tgz", + "integrity": "sha512-wFWC1wyf2ROFWTxK5x0Enm++DSof3EBQ/ypyAesMDLiYxOOASDoMOZG1ylWUnlKaCt5W7eNOWOzABpdfFf/ssA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.17.1.tgz", + "integrity": "sha512-k/hUif0GEBk/csSqCfTPXb8AAVs1NNWCa/skBghvNbTtORcWfOVqJ3mM+2pE189+enRm4UnryLREu5ysI0kXEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.17.1.tgz", + "integrity": "sha512-Cwm6A071ww60QouJ9LoHAwBgEoZzHQ0Qaqk2E7WLfBdiQN9mLXIDhnrpn04hlRElRPhLiu/dtg+o5PPLvaINXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.17.1.tgz", + "integrity": "sha512-+hwlE2v3m0r3sk93SchJL1uyaKcPjf+NGO/TD2DZUDo+chXx7FfaEj0nUMewigSt7oZ2sQN9Z4NJOtUa75HE5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.17.1.tgz", + "integrity": "sha512-bO+rsaE5Ox8cFyeL5Ct5tzot1TnQpFa/Wmu5k+hqBYSH2dNVDGoi0NizBN5QV8kOIC6O5MZr81UG4yW/2FyDTA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.17.1.tgz", + "integrity": "sha512-B/P+hxKQ1oX4YstI9Lyh4PGzqB87Ddqj/A4iyRBbPdXTcxa+WW3oRLx1CsJKLmHPdDk461Hmbghq1Bm3pl+8Aw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.17.1.tgz", + "integrity": "sha512-ulp2H3bFXzd/th2maH+QNKj5qgOhJ3v9Yspdf1svTw3CDOuuTl6sRKsWQ7MUw0vnkSNvQndtflBwVXgzZvURsQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.17.1.tgz", + "integrity": "sha512-LAXYVe3rKk09Zo9YKF2ZLBcH8sz8Oj+JIyiUxiHtq0hiYLMsN6dOpCf2hzQEjPAmsSEA/hdC1PVKeXo+oma8mQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.17.1.tgz", + "integrity": "sha512-3RAhxipMKE8RCSPn7O//sj440i+cYTgYbapLeOoDvQEt6R1QcJjTsFgI4iz99FhVj3YbPxlZmcLB5VW+ipyRTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.17.1.tgz", + "integrity": "sha512-wpjMEubGU8r9VjZTLdZR3aPHaBqTl8Jl8F4DBbgNoZ+yhkhQD1/MGvY70v2TLnAI6kAHSvcqgfvaqKDa2iWsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.17.1.tgz", + "integrity": "sha512-XIE4w17RYAVIgx+9Gs3deTREq5tsmalbatYOOBGNdH7n0DfTE600c7wYXsp7ANc3BPDXsInnOzXDEPCvO1F6cg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.17.1.tgz", + "integrity": "sha512-Lqi5BlHX3zS4bpSOkIbOKVf7DIk6Gvmdifr2OuOI58eUUyP944M8/OyaB09cNpPy9Vukj7nmmhOzj8pwLgAkIg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.17.1.tgz", + "integrity": "sha512-l6lTcLBQVj1HNquFpXSsrkCIM8X5Hlng5YNQJrg00z/KyovvDV5l3OFhoRyZ+aLBQ74zUnMRaJZC7xcBnHyeNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.17.1.tgz", + "integrity": "sha512-VTzVtfnCCsU/6GgvursWoyZrhe3Gj/RyXzDWmh4/U1Y3IW0u1FZbp+hCIlBL16pRPbDc5YvXVtCOnA41QOrOoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.17.1.tgz", + "integrity": "sha512-jRPVU+6/12baj87q2+UGRh30FBVBzqKdJ7rP/mSqiL1kpNQB9yZ1j0+m3sru1m+C8hiFK7lBFwjUtYUBI7+UpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -1967,6 +2261,287 @@ "win32" ] }, + "node_modules/@swc-node/core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.14.1.tgz", + "integrity": "sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@swc/core": ">= 1.13.3", + "@swc/types": ">= 0.1" + } + }, + "node_modules/@swc-node/register": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@swc-node/register/-/register-1.11.1.tgz", + "integrity": "sha512-VQ0hJ5jX31TVv/fhZx4xJRzd8pwn6VvzYd2tGOHHr2TfXGCBixZoqdPDXTiEoJLCTS2MmvBf6zyQZZ0M8aGQCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc-node/core": "^1.14.1", + "@swc-node/sourcemap-support": "^0.6.1", + "colorette": "^2.0.20", + "debug": "^4.4.1", + "oxc-resolver": "^11.6.1", + "pirates": "^4.0.7", + "tslib": "^2.8.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@swc/core": ">= 1.4.13", + "typescript": ">= 4.3" + } + }, + "node_modules/@swc-node/sourcemap-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@swc-node/sourcemap-support/-/sourcemap-support-0.6.1.tgz", + "integrity": "sha512-ovltDVH5QpdHXZkW138vG4+dgcNsxfwxHVoV6BtmTbz2KKl1A8ZSlbdtxzzfNjCjbpayda8Us9eMtcHobm38dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.21", + "tslib": "^2.8.1" + } + }, + "node_modules/@swc/core": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz", + "integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.11", + "@swc/core-darwin-x64": "1.15.11", + "@swc/core-linux-arm-gnueabihf": "1.15.11", + "@swc/core-linux-arm64-gnu": "1.15.11", + "@swc/core-linux-arm64-musl": "1.15.11", + "@swc/core-linux-x64-gnu": "1.15.11", + "@swc/core-linux-x64-musl": "1.15.11", + "@swc/core-win32-arm64-msvc": "1.15.11", + "@swc/core-win32-ia32-msvc": "1.15.11", + "@swc/core-win32-x64-msvc": "1.15.11" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz", + "integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz", + "integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz", + "integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz", + "integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz", + "integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz", + "integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz", + "integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz", + "integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz", + "integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz", + "integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2509,6 +3084,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2557,6 +3139,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2592,6 +3187,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/command-line-args": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", @@ -2700,6 +3302,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3282,7 +3885,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -3321,7 +3923,6 @@ "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3671,6 +4272,15 @@ } } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3736,6 +4346,38 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-resolver": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.17.1.tgz", + "integrity": "sha512-pyRXK9kH81zKlirHufkFhOFBZRks8iAMLwPH8gU7lvKFiuzUH9L8MxDEllazwOb8fjXMcWjY1PMDfMJ2/yh5cw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.17.1", + "@oxc-resolver/binding-android-arm64": "11.17.1", + "@oxc-resolver/binding-darwin-arm64": "11.17.1", + "@oxc-resolver/binding-darwin-x64": "11.17.1", + "@oxc-resolver/binding-freebsd-x64": "11.17.1", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.17.1", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.17.1", + "@oxc-resolver/binding-linux-arm64-gnu": "11.17.1", + "@oxc-resolver/binding-linux-arm64-musl": "11.17.1", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.17.1", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.17.1", + "@oxc-resolver/binding-linux-riscv64-musl": "11.17.1", + "@oxc-resolver/binding-linux-s390x-gnu": "11.17.1", + "@oxc-resolver/binding-linux-x64-gnu": "11.17.1", + "@oxc-resolver/binding-linux-x64-musl": "11.17.1", + "@oxc-resolver/binding-openharmony-arm64": "11.17.1", + "@oxc-resolver/binding-wasm32-wasi": "11.17.1", + "@oxc-resolver/binding-win32-arm64-msvc": "11.17.1", + "@oxc-resolver/binding-win32-ia32-msvc": "11.17.1", + "@oxc-resolver/binding-win32-x64-msvc": "11.17.1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3821,6 +4463,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3880,6 +4532,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", @@ -4079,6 +4741,16 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4088,6 +4760,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4442,12 +5125,43 @@ "dependencies": { "@formatjs/intl-durationformat": "^0.10.1", "@idle-economy/engine": "*", - "command-line-args": "^6.0.1" + "command-line-args": "^6.0.1", + "json5": "^2.2.3", + "moment": "^2.30.1" }, "devDependencies": { + "@swc-node/register": "^1.11.1", + "@swc/core": "^1.15.11", "@types/command-line-args": "^5.2.3" } }, + "simulator-web": { + "name": "@idle-economy/simulator-web", + "version": "0.0.0", + "dependencies": { + "@idle-economy/engine": "*", + "@idle-economy/simulator": "*", + "chart.js": "^4.5.1", + "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.0", + "sass": "^1.97.3" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, "simulator/node_modules/@formatjs/ecma402-abstract": { "version": "3.1.1", "license": "MIT", diff --git a/package.json b/package.json index 47d51f0..dc73686 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "workspaces": [ "engine", "web", - "simulator" + "simulator", + "simulator-web" ], "devDependencies": { "@types/node": "^24.10.0", diff --git a/simulator/package.json b/simulator/package.json index 13a73b8..7e21bde 100644 --- a/simulator/package.json +++ b/simulator/package.json @@ -2,10 +2,10 @@ "name": "@idle-economy/simulator", "version": "1.0.0", "description": "", - "main": "dist/index.js", + "main": "src/index.js", "scripts": { - "start": "tsx ./src/index.ts", - "dev": "tsx watch --include './src/**/*' --include '../engine/src/**/*' ./src/index.ts" + "start": "tsx ./src/run.ts", + "dev": "tsx watch --include './src/**/*' --include '../engine/src/**/*' ./src/run.ts" }, "keywords": [], "author": "", @@ -14,9 +14,13 @@ "dependencies": { "@formatjs/intl-durationformat": "^0.10.1", "@idle-economy/engine": "*", - "command-line-args": "^6.0.1" + "command-line-args": "^6.0.1", + "json5": "^2.2.3", + "moment": "^2.30.1" }, "devDependencies": { + "@swc-node/register": "^1.11.1", + "@swc/core": "^1.15.11", "@types/command-line-args": "^5.2.3" } } diff --git a/simulator/src/ConsoleSimulator.ts b/simulator/src/ConsoleSimulator.ts new file mode 100644 index 0000000..87a9254 --- /dev/null +++ b/simulator/src/ConsoleSimulator.ts @@ -0,0 +1,69 @@ +import { DurationFormat } from '@formatjs/intl-durationformat' +import { Action, Game, ResourceSet, GameSnapshot, makeGame } from '@idle-economy/engine'; +import { formatTime, now } from '../utils/time'; +import { RealtimeStats, Simulation, SimulationOptions, SimulationStats } from './Simulation'; + +const durationFormat = new DurationFormat( + 'en', + { + style: 'long', + hours: 'numeric', + minutes: 'numeric', + seconds: 'numeric', + }, +); + +export class ConsoleSimulator { + + public readonly simulation: Simulation; + + constructor( + options: Omit, + ) { + this.simulation = new Simulation({ + ...options, + onAction: this.handleSimulationAction.bind(this), + onStats: this.handleSimulationStats.bind(this), + }); + } + + private handleSimulationAction(action: Action) { + // this.logAction(action, 0); + } + + private handleSimulationStats(stats: SimulationStats) { + const rt = stats.realtime[stats.realtime.length - 1]; + if (rt) + this.logRealtime(rt); + this.logStats(stats); + } + + private log(time: number, data: string): void { + console.log(`${formatTime(time)}: ${data}`); + } + + private logRealtime(rtStats: RealtimeStats) { + this.log(rtStats.time, `Simulation speed: ${formatTime(rtStats.timeDelta)} / second`); + } + + private logStats(stats: SimulationStats): void { + let lastTime = 0; + let maxDeltaTime = 0; + for (const action of stats.actions) { + const deltaTime = action.time - lastTime; + this.logAction(action, deltaTime); + lastTime = action.time; + maxDeltaTime = Math.max(maxDeltaTime, deltaTime); + } + this.log(stats.finalTime, `max action wait fime is ${formatTime(maxDeltaTime, 'short')}`) + this.log(stats.finalTime, `final data: ${JSON.stringify(stats.finalSnapshot, undefined, 2)}`); + } + + private logResources(time: number, resources: ResourceSet): void { + this.log(time, `RESOURCES ${resources.toString()} `); + } + + private logAction(action: Action, deltaTime: number): void { + this.log(action.time, `ACTION ${action.toString()}, time since last action: ${formatTime(deltaTime, 'short')} `); + } +} diff --git a/simulator/src/Simulation.ts b/simulator/src/Simulation.ts new file mode 100644 index 0000000..7be48ae --- /dev/null +++ b/simulator/src/Simulation.ts @@ -0,0 +1,142 @@ +import { Action, Game, GameSnapshot, makeGame } from '@idle-economy/engine'; +import { Player } from './player/Player'; +import { formatTime, now } from '../utils/time'; +import { defaultGameRulesOptions, GameRulesOptions } from '@idle-economy/engine/src/rulesets/rules'; + +export type RealtimeStats = { + realtime: number, + time: number, + timeDelta: number, +} + +export type SimulationStats = { + realtime: RealtimeStats[], + actions: Action[], + finalTime: number; + finalSnapshot: GameSnapshot, +} + +export type SimulationOptions = { + onAction?: (action: Action) => void, + onStats?: (stats: SimulationStats) => void, +} + +const emptyStats: SimulationStats = { + realtime: [], + actions: [], + finalTime: 0, + finalSnapshot: { resources: {}, generators: [] }, +}; + +export class Simulation { + + public time: number = 0; + + public game: Game; + public player: Player; + public stats: SimulationStats; + + private gameRulesOptions: GameRulesOptions = defaultGameRulesOptions; + + constructor( + private readonly options: SimulationOptions, + ) { + } + + public initialize(gameRulesOptions: Partial) { + this.gameRulesOptions = { ...this.gameRulesOptions, ...gameRulesOptions }; + + this.game = makeGame(this.gameRulesOptions); + this.player = new Player(this.game); + this.reset(); + } + + public reset() { + this.time = 0; + this.game.reset(); + + this.stats = JSON.parse(JSON.stringify(emptyStats)); + this.emitStats(); + } + + private emitAction(action: Action) { + this.options.onAction?.(action); + } + + private emitStats() { + this.options.onStats?.(this.stats); + } + + public async run(duration: number) { + const finishTime = this.time + duration; + + const start = now(); + let lastSecond = -1; + let lastSecondTime = -1; + + let timePassed = 0; + + this.emitStats(); + + while (this.time < finishTime) { + const deltaTime = 1.0 / 60; + + timePassed = now() - start; + const second = Math.floor(timePassed); + if (second != lastSecond) { + if (lastSecond != -1) { + const rtStats = { + realtime: timePassed, + time: this.time, + timeDelta: this.time - lastSecondTime, + } as RealtimeStats; + this.stats.realtime.push(rtStats); + + this.emitStats(); + } + + lastSecond = second; + lastSecondTime = this.time; + } + + const tickActions = Array.from(this.tick(deltaTime)); + if (tickActions.length) { + tickActions.forEach((action) => { + action.time = this.time; + action.gameAfter = this.makeGameSnapshot(); + }); + this.stats.actions.push(...tickActions); + for (let action of tickActions) + this.emitAction(action); + } + + this.time += deltaTime; + } + + this.time = finishTime; + + const rtStats = { + realtime: timePassed, + time: this.time, + timeDelta: this.time - lastSecondTime, + } as RealtimeStats; + this.stats.realtime.push(rtStats); + + this.stats.finalTime = this.time; + this.stats.finalSnapshot = this.makeGameSnapshot(); + + this.emitStats(); + } + + private makeGameSnapshot(): GameSnapshot { + return { + resources: this.game.resources.toObject(), + generators: this.game.generators.map((gen) => gen.snapshot()), + } + } + + private *tick(time: number): Generator { + yield* this.game.tick(time); + yield* this.player.tick(time); + } +} diff --git a/simulator/src/Simulator.ts b/simulator/src/Simulator.ts deleted file mode 100644 index f4be6a4..0000000 --- a/simulator/src/Simulator.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { DurationFormat } from '@formatjs/intl-durationformat' -import { Action, Game, ResourceSet, GameSnapshot } from '@idle-economy/engine'; -import { Player } from './player/Player'; - -const durationFormat = new DurationFormat("en", { style: "short" }); - -export type SimulationStats = { - actions: Action[], - finalTime: number; - finalSnapshot: GameSnapshot, -} - -export class Simulator { - constructor( - public readonly player: Player, - ) { - - } - - public get game(): Game { - return this.player.game; - } - - public simulate(duration: number) { - const stats: SimulationStats = { - actions: [], - finalTime: 0, - finalSnapshot: { resources: {}, generators: [] }, - }; - - let time = 0; - let lastSecond = -1; - - this.log(time, "start"); - - while (time < duration) { - const deltaTime = 1 / 60; - const tickActions = Array.from(this.tick(deltaTime)); - if (tickActions.length) { - const snapshot: GameSnapshot = this.makeGameSnapshot(); - tickActions.forEach((action) => { - action.time = time; - action.gameAfter = snapshot; - }); - stats.actions.push(...tickActions); - } - // for (const tickAction of tickActions) - // this.logAction(time, tickAction); - - // this.logResources(time, this.game.resources); - - time += deltaTime; - } - - stats.finalTime = time; - stats.finalSnapshot = this.makeGameSnapshot(); - - this.log(time, "done"); - - this.logStats(stats); - } - - private makeGameSnapshot(): GameSnapshot { - return { - resources: this.game.resources.toObject(), - generators: this.game.generators.map((gen) => gen.snapshot()), - } - } - - private *tick(time: number): Generator { - yield* this.player.tick(time); - yield* this.game.tick(time); - } - - private log(time: number, data: string): void { - console.log(`${Simulator.formatTime(time)}: ${data}`); - } - - private logStats(stats: SimulationStats): void { - let lastTime = 0; - let maxDeltaTime = 0; - for (const action of stats.actions) { - const deltaTime = action.time - lastTime; - this.logAction(action, deltaTime); - lastTime = action.time; - maxDeltaTime = Math.max(maxDeltaTime, deltaTime); - } - this.log(stats.finalTime, `max action wait fime is ${maxDeltaTime}`) - this.log(stats.finalTime, `final data: ${JSON.stringify(stats.finalSnapshot, undefined, 2)}`); - } - - private logResources(time: number, resources: ResourceSet): void { - this.log(time, `RESOURCES ${resources.toString()} `); - } - - private logAction(action: Action, deltaTime: number): void { - this.log(action.time, `ACTION ${action.toString()}, time since last action: ${Simulator.formatTime(deltaTime)} `); - } - - private static formatTime(time: number): string { - return durationFormat.format({ milliseconds: Math.round(time * 1000) }); - } -} \ No newline at end of file diff --git a/simulator/src/actions/index.ts b/simulator/src/actions/index.ts new file mode 100644 index 0000000..e7723d9 --- /dev/null +++ b/simulator/src/actions/index.ts @@ -0,0 +1,2 @@ +export * from './PlayerAction'; +export * from './ResourceGeneratorUpgradeAction'; diff --git a/simulator/src/index.ts b/simulator/src/index.ts index 76dbc44..e4454ac 100644 --- a/simulator/src/index.ts +++ b/simulator/src/index.ts @@ -1,21 +1,3 @@ -import { makeGame } from '@idle-economy/engine'; -import { Simulator } from './Simulator' -import { Player } from './player/Player'; -import commandLineArgs from 'command-line-args' - -process.on('SIGTERM', () => { - // Clean up resources and exit - process.exit(0); -}); - -const optionDefinitions: commandLineArgs.OptionDefinition[] = [ - { name: 'time', alias: 't', type: Number, defaultValue: 60 }, -]; -const options = commandLineArgs(optionDefinitions); - -const game = makeGame(); - -const player = new Player(game); - -const simulator = new Simulator(player); -simulator.simulate(options.time); +export * from './player/Player'; +export * from './Simulation'; +export * from './actions'; diff --git a/simulator/src/player/Player.ts b/simulator/src/player/Player.ts index 238a1ea..5bcbbd9 100644 --- a/simulator/src/player/Player.ts +++ b/simulator/src/player/Player.ts @@ -19,7 +19,10 @@ export class Player { } private *tryUpgradeResourceGenerator(generator: ResourceGenerator): Generator { - if (generator.upgrade()) { + // const upgradeLevelsAvailable = generator.upgradeLevelsAvailable('max'); + const upgradeLevelsAvailable = generator.upgradeLevelsAvailable(1); + for (let i = 0; i < upgradeLevelsAvailable; i++) { + generator.upgrade(); yield new ResourceGeneratorUpgradeAction(generator.snapshot()); } } diff --git a/simulator/src/run.ts b/simulator/src/run.ts new file mode 100644 index 0000000..df5143a --- /dev/null +++ b/simulator/src/run.ts @@ -0,0 +1,21 @@ +import { defaultGameRulesOptions } from '@idle-economy/engine/src/rulesets/rules'; +import { ConsoleSimulator } from './ConsoleSimulator'; +import commandLineArgs from 'command-line-args'; +import json5 from 'json5'; + +process.on('SIGTERM', () => { + // Clean up resources and exit + process.exit(0); +}); + +const optionDefinitions: commandLineArgs.OptionDefinition[] = [ + { name: 'time', alias: 't', type: Number, defaultValue: 60 }, + { name: 'rules', alias: 'r', type: String, defaultValue: JSON.stringify(defaultGameRulesOptions) }, +]; +const options = commandLineArgs(optionDefinitions); + +(async () => { + const simulator = new ConsoleSimulator({}); + simulator.simulation.initialize(json5.parse(options.rules)); + await simulator.simulation.run(options.time); +})(); diff --git a/simulator/tsconfig.json b/simulator/tsconfig.json new file mode 100644 index 0000000..e8c2d93 --- /dev/null +++ b/simulator/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "nodenext", + "moduleResolution": "nodenext", + "skipLibCheck": true + } +} diff --git a/simulator/utils/time.ts b/simulator/utils/time.ts new file mode 100644 index 0000000..5cb6986 --- /dev/null +++ b/simulator/utils/time.ts @@ -0,0 +1,14 @@ +import moment from "moment"; + +export function now(): number { + return (performance.now() + performance.timeOrigin) / 1000; +} + +export function formatTime(time: number, format: 'full' | 'short' = 'full'): string { + if (format === 'short') + return moment.utc(time * 1000).format('HH:mm:ss.SSS'); + else + return moment.utc(time * 1000).format('HH:mm:ss'); + // return moment.utc(time * 1000).format('HH:mm:ss.SSS'); + // return durationFormat.format({ seconds: Math.round(time * 1000) }); +} diff --git a/web/src/ResourceGeneratorView.tsx b/web/src/ResourceGeneratorView.tsx index 9547ee8..8ba7627 100644 --- a/web/src/ResourceGeneratorView.tsx +++ b/web/src/ResourceGeneratorView.tsx @@ -2,6 +2,7 @@ import { type ResourceGenerator } from "@idle-economy/engine"; import { observer } from "mobx-react-lite"; import { ResourcesView } from "./ResourcesView"; import { root } from "./state/Root"; +import { formatNumber } from "./tools/format"; type Props = { generator: ResourceGenerator, @@ -12,8 +13,10 @@ export const ResourceGeneratorView = observer(function (props: Props) { let gen = props.generator; let showName = gen.rule.name === 'prestige'; - const upgradeLevelsAvailable = gen.upgradeLevelsAvailable(root.upgradeStep); - const upgradeToLevel = gen.level + upgradeLevelsAvailable; + const realUpgradeLevelsAvailable = gen.upgradeLevelsAvailable(root.upgradeStep); + const isUpgradable = realUpgradeLevelsAvailable === 0; + const upgradeLevelsAvailable = Math.max(1, realUpgradeLevelsAvailable); + const upgradeToLevel = gen.level + (realUpgradeLevelsAvailable > 0 ? upgradeLevelsAvailable : (root.upgradeStep === 'max' ? 1 : root.upgradeStep)); function handleGeneratorClick(): void { if (props.generator.rule.manualGeneration) @@ -36,7 +39,7 @@ export const ResourceGeneratorView = observer(function (props: Props) { >
{gen.rule.name}
LEVEL {gen.level}
-
+ / {gen.getPeriod()} sec
+
+ / {formatNumber(gen.getPeriod())} sec
{/*
*/} {/*
{gen.isVisible ? 'visible' : 'not visible'}
{gen.isUpgradable ? 'upgradable' : 'not upgradable'}
@@ -45,12 +48,16 @@ export const ResourceGeneratorView = observer(function (props: Props) { ) diff --git a/web/src/state/Root.ts b/web/src/state/Root.ts index 5d3ef96..990e531 100644 --- a/web/src/state/Root.ts +++ b/web/src/state/Root.ts @@ -2,10 +2,21 @@ import { makeGame, ResourceGenerator, ResourceSet } from "@idle-economy/engine"; import { makeAutoObservable } from "mobx"; import { Notation, Presets } from 'eternal_notations'; import { now } from "../tools/time"; +import { defaultGameRulesOptions, type GameRulesOptions } from "@idle-economy/engine/src/rulesets/rules"; + +const gameRulesOptions: GameRulesOptions = defaultGameRulesOptions; + +// const gameRulesOptions: GameRulesOptions = { +// startingResources: 0, +// generationBasePrice: 1, +// generationPower: 1.2, +// upgradeBasePrice: 2000, +// upgradeFactor: 1.01, +// } export class Root { - private readonly game = makeGame(); + private readonly game = makeGame(gameRulesOptions); public resources: ResourceSet = new ResourceSet(); public generators: ResourceGenerator[] = []; diff --git a/web/src/tools/format.ts b/web/src/tools/format.ts index 1d0c989..05a5da4 100644 --- a/web/src/tools/format.ts +++ b/web/src/tools/format.ts @@ -1,6 +1,11 @@ -import type Decimal from 'break_eternity.js'; import { root } from '../state/Root'; +import Decimal, { type DecimalSource } from 'break_eternity.js'; +import { Presets } from 'eternal_notations'; -export function formatNumber(value: Decimal | undefined): string { - return root.numberNotation.format(value ?? 0); +export function formatNumber(value: DecimalSource | undefined): string { + const v = new Decimal(value ?? 0); + if (v.lt(1)) + return Presets.Default.format(v) + else + return root.numberNotation.format(v); } \ No newline at end of file