171 lines
7.1 KiB
JavaScript
171 lines
7.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { buyEndless, endlessQuote, lineAvailable } from '../public/js/engine/endless.js';
|
|
import { advance, derive, generatorQuote } from '../public/js/engine/engine.js';
|
|
import { bulkCost } from '../public/js/engine/formulas.js';
|
|
import { prestige } from '../public/js/engine/legacy.js';
|
|
import { createState, sanitizeState, serializeState } from '../public/js/engine/state.js';
|
|
import { loadContext } from './helpers.js';
|
|
|
|
const ctx = loadContext();
|
|
const fresh = () => createState(ctx, 1_000_000);
|
|
const line = (id) => ctx.endlessById.get(id);
|
|
|
|
/** A state that owns every milestone upgrade, so every endless line is open. */
|
|
function finished() {
|
|
const s = fresh();
|
|
s.upgrades = ctx.upgrades.map((u) => u.id);
|
|
return s;
|
|
}
|
|
|
|
test('a line opens only once its milestone upgrade is owned', () => {
|
|
const s = fresh();
|
|
assert.equal(lineAvailable(s, line('e_global')), false);
|
|
assert.equal(buyEndless(s, ctx, 'e_global'), false);
|
|
s.upgrades = ['global_4'];
|
|
s.funds = 1e12;
|
|
assert.equal(lineAvailable(s, line('e_global')), true);
|
|
assert.ok(buyEndless(s, ctx, 'e_global'));
|
|
assert.equal(s.endless.e_global, 1);
|
|
});
|
|
|
|
test('every milestone family continues in an endless line', () => {
|
|
const fed = new Set(ctx.endless.map((l) => l.requires));
|
|
for (const id of ['click_3', 'g1_b', 'g2_b', 'g3_b', 'g4_b', 'g5_b', 'g6_b', 'global_4', 'demand_4', 'value_3', 'mat_eff_2', 'mat_disc_2', 'stack_5']) {
|
|
assert.ok(fed.has(id), `${id} leads to an endless line`);
|
|
}
|
|
});
|
|
|
|
test('costs grow exponentially, level after level', () => {
|
|
const s = finished();
|
|
const l = line('e_global');
|
|
const costs = [0, 1, 2, 10, 50].map((level) => {
|
|
s.endless.e_global = level;
|
|
return endlessQuote(s, ctx, 'e_global', 1).cost;
|
|
});
|
|
assert.equal(costs[0], Math.ceil(l.baseCost));
|
|
for (let i = 1; i < costs.length; i++) assert.ok(costs[i] > costs[i - 1]);
|
|
assert.ok(Math.abs(costs[2] / costs[1] - l.costGrowth) < 0.001);
|
|
assert.ok(costs[4] / costs[0] > l.costGrowth ** 49);
|
|
});
|
|
|
|
test('buying several levels charges the exact sum', () => {
|
|
const s = finished();
|
|
s.funds = 1e15;
|
|
const l = line('e_demand');
|
|
const before = s.funds;
|
|
assert.ok(buyEndless(s, ctx, 'e_demand', 10));
|
|
assert.equal(s.endless.e_demand, 10);
|
|
assert.equal(before - s.funds, bulkCost(l.baseCost, l.costGrowth, 0, 10));
|
|
});
|
|
|
|
test('"max" buys every affordable level and no more', () => {
|
|
const s = finished();
|
|
s.funds = 1e15;
|
|
assert.ok(buyEndless(s, ctx, 'e_value', 'max'));
|
|
const level = s.endless.e_value;
|
|
assert.ok(level > 5);
|
|
assert.ok(s.funds >= 0);
|
|
assert.ok(endlessQuote(s, ctx, 'e_value', 1).cost > s.funds, 'the next level is out of reach');
|
|
});
|
|
|
|
test('each level multiplies the effect again', () => {
|
|
const s = finished();
|
|
s.generators.gen3 = 10;
|
|
const base = derive(s, ctx).rate;
|
|
s.endless.e_gen3 = 4;
|
|
assert.ok(Math.abs(derive(s, ctx).rate / base - 1.5 ** 4) < 1e-9);
|
|
s.endless.e_global = 3;
|
|
const global = line('e_global').effect.value;
|
|
assert.ok(Math.abs(derive(s, ctx).rate / base - 1.5 ** 4 * global ** 3) < 1e-9);
|
|
s.endless.e_demand = 5;
|
|
const demand = line('e_demand').effect.value;
|
|
assert.ok(Math.abs(derive(s, ctx).demandMultiplier / derive(finished(), ctx).demandMultiplier - demand ** 5) < 1e-9);
|
|
});
|
|
|
|
test('discount lines make material cheaper, cost lines make automation cheaper, with floors', () => {
|
|
const s = finished();
|
|
const batch = derive(s, ctx).batchCost;
|
|
const quote = generatorQuote(s, ctx, 'gen2', 1).cost;
|
|
s.endless.e_discount = 10;
|
|
s.endless.e_cost = 10;
|
|
assert.ok(derive(s, ctx).batchCost < batch);
|
|
assert.ok(generatorQuote(s, ctx, 'gen2', 1).cost < quote);
|
|
s.endless.e_cost = 10000;
|
|
s.endless.e_discount = 10000;
|
|
assert.ok(generatorQuote(s, ctx, 'gen2', 1).cost >= quote * 0.04, 'never free');
|
|
assert.ok(derive(s, ctx).batchCost >= 0.01);
|
|
});
|
|
|
|
test('the level cap is respected and an infinite price is never payable', () => {
|
|
const s = finished();
|
|
s.funds = 1e300;
|
|
s.endless.e_click = 9999;
|
|
assert.equal(endlessQuote(s, ctx, 'e_click', 50).count, 1, 'capped at the maximum level');
|
|
s.endless.e_click = 5000;
|
|
assert.equal(endlessQuote(s, ctx, 'e_click', 1).cost, Infinity);
|
|
assert.equal(buyEndless(s, ctx, 'e_click', 1), false);
|
|
s.endless.e_click = line('e_click').maxLevel;
|
|
assert.equal(buyEndless(s, ctx, 'e_click', 'max'), false);
|
|
});
|
|
|
|
test('even absurd levels never produce NaN or Infinity', () => {
|
|
const s = finished();
|
|
for (const l of ctx.endless) s.endless[l.id] = l.maxLevel;
|
|
for (const g of ctx.generators) s.generators[g.id] = 100;
|
|
s.marketing = 300;
|
|
const d = derive(s, ctx);
|
|
for (const key of ['rate', 'demand', 'demandMultiplier', 'valueMultiplier', 'batchSize', 'batchCost', 'materialPerUnit', 'clickYield']) {
|
|
assert.ok(Number.isFinite(d[key]), `${key} = ${d[key]}`);
|
|
}
|
|
for (const v of Object.values(d.perGenerator)) assert.ok(Number.isFinite(v));
|
|
advance(s, ctx, 5);
|
|
for (const key of ['units', 'funds', 'material', 'totalProduced']) {
|
|
assert.ok(!Number.isNaN(s[key]), `${key} is NaN`);
|
|
}
|
|
});
|
|
|
|
test('endless levels belong to the run: a legacy restart resets them', () => {
|
|
const s = finished();
|
|
s.endless.e_global = 12;
|
|
s.totalProduced = s.lifetimeProduced = 1e14;
|
|
prestige(s, ctx);
|
|
assert.deepEqual(s.endless, {});
|
|
});
|
|
|
|
test('saved levels are cleaned up and survive a round trip', () => {
|
|
const s = finished();
|
|
s.endless = { e_global: 25, e_gen4: 7 };
|
|
const back = sanitizeState(JSON.parse(serializeState(s)), ctx);
|
|
assert.deepEqual(back.endless, { e_global: 25, e_gen4: 7 });
|
|
|
|
const dirty = sanitizeState({ endless: { e_global: 1e9, e_click: -3, nope: 5, e_gen1: 'x', e_value: 2.9 } }, ctx);
|
|
assert.deepEqual(dirty.endless, { e_global: line('e_global').maxLevel, e_value: 2 });
|
|
assert.deepEqual(sanitizeState({}, ctx).endless, {}, 'older saves have none');
|
|
});
|
|
|
|
test('growth stays open-ended without ever running away', () => {
|
|
// Income is the product of several lines, so what matters is the sum, over the
|
|
// lines that raise income, of (log of the effect) / (log of the cost growth),
|
|
// weighted by how much of income each one drives. Above 1 the economy blows up
|
|
// (it did, in an earlier tuning: 1e207/s after two hours); keep a margin below it.
|
|
const ratio = (l) => Math.log(l.effect.value) / Math.log(l.costGrowth);
|
|
const weights = { e_global: 2 / 3, e_demand: 1 / 3, e_value: 1 };
|
|
let sum = Object.entries(weights).reduce((acc, [id, w]) => acc + w * ratio(line(id)), 0);
|
|
|
|
// A generator line only drives its tier's share of production (equal counts per tier).
|
|
const totalRate = ctx.generators.reduce((acc, g) => acc + g.baseRate, 0);
|
|
for (const l of ctx.endless.filter((x) => x.effect.target)) {
|
|
sum += (ctx.generatorsById.get(l.effect.target).baseRate / totalRate) * ratio(l);
|
|
}
|
|
// The existing marketing levels also feed demand.
|
|
const marketing = Math.log(ctx.rules.marketing.demandPerLevel) / Math.log(ctx.rules.marketing.costGrowth);
|
|
sum += marketing / 3;
|
|
|
|
assert.ok(sum < 0.95, 'income-driving ratios add up to ' + sum.toFixed(2));
|
|
for (const l of ctx.endless) {
|
|
assert.ok(l.costGrowth > 1.5, l.id + ' must get costly steeply');
|
|
assert.equal(l.maxLevel, 10000);
|
|
}
|
|
});
|