initial commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@idle-economy/engine",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"break_eternity.js": "^2.1.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { GameSnapshot } from '../types';
|
||||
|
||||
export abstract class Action {
|
||||
public time: number = 0;
|
||||
public gameAfter: GameSnapshot = { resources: { RED: 0 }, generators: [] };
|
||||
|
||||
constructor(
|
||||
public readonly name: string
|
||||
) {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `${this.name} (${this.toStringDescription()})`;
|
||||
}
|
||||
|
||||
protected abstract toStringDescription(): string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Action } from "./Action";
|
||||
|
||||
export abstract class GameAction extends Action {
|
||||
constructor(
|
||||
name: string,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Action';
|
||||
export * from './GameAction';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Action, GameAction } from "../actions";
|
||||
import { type GameRules } from "../rules";
|
||||
import { ResourceGenerator } from "./ResourceGenerator";
|
||||
import { ResourceSet } from "./ResourceSet";
|
||||
|
||||
export class Game {
|
||||
|
||||
public readonly rules: GameRules;
|
||||
|
||||
public readonly resources = new ResourceSet();
|
||||
public readonly generators: ResourceGenerator[] = [];
|
||||
|
||||
constructor(
|
||||
rules: GameRules,
|
||||
) {
|
||||
this.rules = rules;
|
||||
|
||||
this.initGenerators();
|
||||
}
|
||||
|
||||
private initGenerators(): void {
|
||||
this.generators.splice(0);
|
||||
this.generators.push(...this.rules.generators.map((rule) => new ResourceGenerator(this, rule)));
|
||||
}
|
||||
|
||||
public *tick(time: number): Generator<Action, void, unknown> {
|
||||
yield* this.runResourceGenerators(time);
|
||||
}
|
||||
|
||||
private *runResourceGenerators(time: number): Generator<GameAction, void, unknown> {
|
||||
for (const gen of this.generators)
|
||||
gen.tick(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { ResourceGeneratorRule } from "../rules";
|
||||
import type { ResourceGeneratorSnapshot } from "../types";
|
||||
import { Game } from "./Game";
|
||||
import { ResourceSet } from "./ResourceSet";
|
||||
|
||||
export class ResourceGenerator {
|
||||
|
||||
private _level: number;
|
||||
private _progress: number;
|
||||
|
||||
public readonly game: Game;
|
||||
public readonly rule: ResourceGeneratorRule;
|
||||
|
||||
public get level() { return this._level };
|
||||
|
||||
public get progress() { return this._progress };
|
||||
|
||||
constructor(
|
||||
game: Game,
|
||||
rule: ResourceGeneratorRule,
|
||||
) {
|
||||
this.game = game;
|
||||
this.rule = rule;
|
||||
this._level = this.rule.startingLevel;
|
||||
this._progress = 0;
|
||||
}
|
||||
|
||||
public tick(time: number): void {
|
||||
if (this.period === 0) {
|
||||
this._progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this._progress = (this._progress + time) % this.period;
|
||||
this.game.resources.add(this.generation.times(time / this.period));
|
||||
}
|
||||
|
||||
public upgrade(): boolean {
|
||||
if (this.isOpen && this.canAffordUpgrade) {
|
||||
this.game.resources.remove(this.upgradePrice);
|
||||
this.increaseLevel();
|
||||
if (this.rule.resetProgressOnUpgrade)
|
||||
this._progress = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private increaseLevel() {
|
||||
this._level++;
|
||||
}
|
||||
|
||||
public get period(): number {
|
||||
return this.rule.period(this.level);
|
||||
}
|
||||
|
||||
public get generation(): ResourceSet {
|
||||
return ResourceSet.from(this.rule.generation(this.level));
|
||||
}
|
||||
|
||||
public get visibilityPrice(): ResourceSet {
|
||||
return ResourceSet.from(this.rule.visibilityPrice(this.level));
|
||||
}
|
||||
|
||||
public get openPrice(): ResourceSet {
|
||||
return ResourceSet.from(this.rule.openPrice(this.level));
|
||||
}
|
||||
|
||||
public get upgradePrice(): ResourceSet {
|
||||
return ResourceSet.from(this.rule.upgradePrice(this.level));
|
||||
}
|
||||
|
||||
public get isVisible(): boolean {
|
||||
return this.game.resources.gte(this.visibilityPrice);
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this.game.resources.gte(this.openPrice);
|
||||
}
|
||||
|
||||
public get canAffordUpgrade(): boolean {
|
||||
return this.game.resources.gte(this.upgradePrice);
|
||||
}
|
||||
|
||||
public snapshot(): ResourceGeneratorSnapshot {
|
||||
return {
|
||||
name: this.rule.name,
|
||||
level: this.level,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ResourceNames, type PartialResources, type Resource } from "../types";
|
||||
|
||||
export class ResourceSet {
|
||||
private items = new Map<Resource, number>();
|
||||
|
||||
public static from(source: PartialResources): ResourceSet {
|
||||
const inst = new ResourceSet();
|
||||
inst.items = new Map<Resource, number>(Object.entries(source) as [Resource, number][]);
|
||||
return inst;
|
||||
}
|
||||
|
||||
public clone(): ResourceSet {
|
||||
return ResourceSet.from(this.toObject());
|
||||
}
|
||||
|
||||
public get(key: Resource): number {
|
||||
return this.items.get(key) ?? 0;
|
||||
}
|
||||
|
||||
public set(key: Resource, value: number): void {
|
||||
this.items.set(key, value);
|
||||
}
|
||||
|
||||
public predicate(predicate: (res: Resource) => boolean): boolean {
|
||||
return ResourceNames.every((res) => predicate(res));
|
||||
}
|
||||
|
||||
public gte(arg: ResourceSet): boolean {
|
||||
return this.predicate((res) => this.get(res) >= arg.get(res));
|
||||
}
|
||||
|
||||
public static transform(target: ResourceSet, predicate: (res: Resource) => number): void {
|
||||
for (const res of ResourceNames)
|
||||
target.set(res, predicate(res));
|
||||
}
|
||||
|
||||
public sum(arg: ResourceSet): ResourceSet {
|
||||
const inst = new ResourceSet();
|
||||
ResourceSet.transform(inst, (res) => this.get(res) + arg.get(res));
|
||||
return inst;
|
||||
}
|
||||
|
||||
public add(arg: ResourceSet): void {
|
||||
ResourceSet.transform(this, (res) => this.get(res) + arg.get(res));
|
||||
}
|
||||
|
||||
public remove(arg: ResourceSet): void {
|
||||
ResourceSet.transform(this, (res) => this.get(res) - arg.get(res));
|
||||
}
|
||||
|
||||
public times(factor: number): ResourceSet {
|
||||
const inst = new ResourceSet();
|
||||
ResourceSet.transform(inst, (res) => this.get(res) * factor);
|
||||
return inst;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return Array.from(this.items).map(([k, v]) => `${v} ${k}`).join(', ');
|
||||
}
|
||||
|
||||
public toObject(): PartialResources {
|
||||
return Object.fromEntries(this.items) as PartialResources;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './ResourceSet';
|
||||
export * from './Game';
|
||||
export * from './ResourceGenerator';
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Game } from './game';
|
||||
import type { ResourceGeneratorRule } from './rules';
|
||||
import type { PartialResources, Resource } from './types';
|
||||
|
||||
export * from './types';
|
||||
export * from './rules';
|
||||
export * from './game';
|
||||
export * from './actions';
|
||||
|
||||
type ResourceGenParams = {
|
||||
offset?: number,
|
||||
power?: number,
|
||||
factor?: number,
|
||||
}
|
||||
|
||||
type SimpleResGenParams = {
|
||||
name: string,
|
||||
startingLevel?: number,
|
||||
period: number,
|
||||
generation: Partial<Record<Resource, ResourceGenParams>>,
|
||||
visibilityPrice?: Partial<Record<Resource, ResourceGenParams>>,
|
||||
openPrice: Partial<Record<Resource, ResourceGenParams>>,
|
||||
upgradePrice?: Partial<Record<Resource, ResourceGenParams>>,
|
||||
}
|
||||
|
||||
function simpleResGen(params: SimpleResGenParams): ResourceGeneratorRule {
|
||||
|
||||
function resourceGen(p?: Partial<Record<Resource, ResourceGenParams>>): (lvl: number) => PartialResources {
|
||||
if (!p)
|
||||
return () => ({});
|
||||
|
||||
return (lvl) => Object.fromEntries(
|
||||
Object.entries(p)
|
||||
.map(([res, rp]) => [
|
||||
res,
|
||||
(rp.offset ?? 0) + Math.pow(lvl, rp.power ?? 1) * (rp.factor ?? 0),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: params.name,
|
||||
startingLevel: params.startingLevel ?? 0,
|
||||
resetProgressOnUpgrade: true,
|
||||
period: (level) => level ? params.period : 0,
|
||||
generation: resourceGen(params.generation),
|
||||
visibilityPrice: resourceGen(params.visibilityPrice),
|
||||
openPrice: resourceGen(params.openPrice),
|
||||
upgradePrice: resourceGen(params.upgradePrice),
|
||||
}
|
||||
}
|
||||
|
||||
export function makeGame(): Game {
|
||||
return new Game({
|
||||
generators: [
|
||||
simpleResGen({
|
||||
name: 'red',
|
||||
startingLevel: 1,
|
||||
period: 1,
|
||||
generation: { RED: { offset: 3, power: 1.5, factor: 1 } },
|
||||
openPrice: { RED: { offset: 10, power: 2.1, factor: 10 } }
|
||||
}),
|
||||
...['orange', 'yellow', 'green', 'cyan', 'blue', 'violet', ...Array(20)]
|
||||
.map((color, idx) => simpleResGen({
|
||||
name: color,
|
||||
period: idx + 2,
|
||||
generation: { RED: { power: 1.5, factor: (idx + 2) ** 2 } },
|
||||
// visibilityPrice: { RED: { offset: 5 * 10 ** (idx + 1) } },
|
||||
openPrice: { RED: { offset: 10 ** (idx + 2), power: 2.1, factor: 10 * 5 ** (idx + 1) } }
|
||||
})),
|
||||
// simpleResGen({
|
||||
// name: 'blue',
|
||||
// period: 2,
|
||||
// generation: { RED: { factor: 4 } },
|
||||
// visibilityPrice: { RED: { offset: 50 } },
|
||||
// openPrice: { RED: { offset: 100, power: 2.1, factor: 50 } }
|
||||
// }),
|
||||
// simpleResGen({
|
||||
// name: 'green',
|
||||
// period: 3,
|
||||
// generation: { RED: { factor: 9 } },
|
||||
// visibilityPrice: { RED: { offset: 500 } },
|
||||
// openPrice: { RED: { offset: 1000, power: 2.1, factor: 250 } }
|
||||
// }),
|
||||
// {
|
||||
// name: "red",
|
||||
// startingLevel: 1,
|
||||
// period: (level) => level ? 1 : 0,
|
||||
// resetProgressOnUpgrade: true,
|
||||
// generation: (level) => {
|
||||
// return { RED: level + 3 };
|
||||
// },
|
||||
// visibilityPrice: (level) => { return {} },
|
||||
// openPrice: (level) => {
|
||||
// return { RED: 10 + Math.pow(level, 2.1) * 10 };
|
||||
// },
|
||||
// upgradePrice: (level) => {
|
||||
// return {};
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "blue",
|
||||
// startingLevel: 0,
|
||||
// period: (level) => level ? 2 : 0,
|
||||
// resetProgressOnUpgrade: true,
|
||||
// generation: (level) => {
|
||||
// return { RED: level * 4 };
|
||||
// },
|
||||
// visibilityPrice: (level) => { return { RED: 50 } },
|
||||
// openPrice: (level) => {
|
||||
// return { RED: 100 + Math.pow(level, 2.1) * 50 };
|
||||
// },
|
||||
// upgradePrice: (level) => {
|
||||
// return {};
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "green",
|
||||
// startingLevel: 0,
|
||||
// period: (level) => level ? 3 : 0,
|
||||
// resetProgressOnUpgrade: true,
|
||||
// generation: (level) => {
|
||||
// return { RED: level * 9 };
|
||||
// },
|
||||
// visibilityPrice: (level) => { return { RED: 500 } },
|
||||
// openPrice: (level) => {
|
||||
// return { RED: 1000 + Math.pow(level, 2.1) * 250 };
|
||||
// },
|
||||
// upgradePrice: (level) => {
|
||||
// return {};
|
||||
// },
|
||||
// },
|
||||
],
|
||||
})
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ResourceGeneratorRule } from "./ResourceGeneratorRule";
|
||||
|
||||
export type GameRules = {
|
||||
generators: ResourceGeneratorRule[],
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { PartialResources } from "../types";
|
||||
|
||||
export type ResourceGeneratorRule = {
|
||||
name: string,
|
||||
startingLevel: number, // usually zero
|
||||
period: (level: number) => number,
|
||||
generation: (level: number) => PartialResources, // per period
|
||||
visibilityPrice: (level: number) => PartialResources;
|
||||
openPrice: (level: number) => PartialResources;
|
||||
upgradePrice: (level: number) => PartialResources;
|
||||
resetProgressOnUpgrade: boolean;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './GameRules';
|
||||
export * from './ResourceGeneratorRule';
|
||||
@@ -0,0 +1,5 @@
|
||||
export const ResourceNames = ['RED'] as const;
|
||||
export type Resource = (typeof ResourceNames)[number];
|
||||
|
||||
export type Resources = Record<Resource, number>;
|
||||
export type PartialResources = Partial<Record<Resource, number>>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Resource';
|
||||
export * from './snapshots';
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Resources } from '../Resource';
|
||||
import { ResourceGeneratorSnapshot } from './ResourceGeneratorSnapshot';
|
||||
|
||||
|
||||
export type GameSnapshot = {
|
||||
resources: Partial<Resources>;
|
||||
generators: ResourceGeneratorSnapshot[];
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ResourceGeneratorSnapshot = {
|
||||
name: string;
|
||||
level: number;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './GameSnapshot';
|
||||
export * from './ResourceGeneratorSnapshot';
|
||||
Reference in New Issue
Block a user