parametric game fules. minor simulator refactoring
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')} `);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './PlayerAction';
|
||||
export * from './ResourceGeneratorUpgradeAction';
|
||||
+3
-21
@@ -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';
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -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) });
|
||||
}
|
||||
Reference in New Issue
Block a user