parametric game fules. minor simulator refactoring

This commit is contained in:
azykov@mail.ru
2026-02-19 22:19:05 +03:00
parent ef5caab748
commit d07f2043e7
22 changed files with 1170 additions and 181 deletions
+8 -4
View File
@@ -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"
}
}
+69
View File
@@ -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<SimulationOptions, 'onAction' | 'onStats'>,
) {
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')} `);
}
}
+142
View File
@@ -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<GameRulesOptions>) {
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<Action, void, unknown> {
yield* this.game.tick(time);
yield* this.player.tick(time);
}
}
-103
View File
@@ -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<Action, void, unknown> {
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) });
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './PlayerAction';
export * from './ResourceGeneratorUpgradeAction';
+3 -21
View File
@@ -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';
+4 -1
View File
@@ -19,7 +19,10 @@ export class Player {
}
private *tryUpgradeResourceGenerator(generator: ResourceGenerator): Generator<ResourceGeneratorUpgradeAction, void, unknown> {
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());
}
}
+21
View File
@@ -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);
})();
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"skipLibCheck": true
}
}
+14
View File
@@ -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) });
}