Files
idle_clicker/tests/engine.test.js
T

196 lines
6.0 KiB
JavaScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
advance,
buyGenerator,
buyMarketing,
buyMaterial,
buyUpgrade,
click,
derive,
generatorQuote,
setPrice,
simulateOffline,
upgradeVisible,
} from '../public/js/engine/engine.js';
import { bulkCost, demandPerSecond, maxAffordable } from '../public/js/engine/formulas.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);
test('bulkCost matches buying one at a time', () => {
let owned = 3;
let total = 0;
for (let i = 0; i < 10; i++) total += Math.ceil(10 * 1.15 ** owned++);
assert.ok(Math.abs(bulkCost(10, 1.15, 3, 10) - total) <= 10);
});
test('maxAffordable never overspends and is maximal', () => {
for (const budget of [0, 4, 5, 100, 12345, 9e9]) {
const n = maxAffordable(5, 1.15, 7, budget);
assert.ok(bulkCost(5, 1.15, 7, n) <= budget);
assert.ok(bulkCost(5, 1.15, 7, n + 1) > budget);
}
});
test('demand is flat at/below the fair price and drops above it', () => {
const { market } = ctx.rules;
const atFair = demandPerSecond(market, market.fairPrice, 1);
assert.equal(demandPerSecond(market, market.fairPrice / 5, 1), atFair);
assert.ok(demandPerSecond(market, market.fairPrice * 2, 1) < atFair / 3);
});
test('click makes a unit from material', () => {
const s = fresh();
assert.ok(click(s, ctx));
assert.equal(s.units, 1);
assert.equal(s.material, ctx.rules.start.material - 1);
assert.equal(s.totalProduced, 1);
});
test('click does nothing without material', () => {
const s = fresh();
s.material = 0;
assert.equal(click(s, ctx), false);
assert.equal(s.units, 0);
});
test('selling turns stock into funds at the set price', () => {
const s = fresh();
s.units = 100;
setPrice(s, ctx, 0.25);
const { sold, revenue } = advance(s, ctx, 1);
assert.ok(sold > 0);
assert.ok(Math.abs(revenue - sold * 0.25) < 1e-9);
assert.ok(Math.abs(s.funds - revenue) < 1e-9);
});
test('generators produce, consuming material', () => {
const s = fresh();
s.funds = 1000;
assert.ok(buyGenerator(s, ctx, 'gen1', 3));
const before = s.material;
advance(s, ctx, 10);
assert.ok(s.totalProduced > 0);
assert.ok(s.material < before);
});
test('production stops when material runs out', () => {
const s = fresh();
s.generators.gen1 = 10;
s.material = 5;
advance(s, ctx, 60);
assert.ok(s.totalProduced <= 5 + 1e-9);
assert.ok(s.material >= 0);
});
test('buying is refused when funds are short, and charges when it works', () => {
const s = fresh();
assert.equal(buyGenerator(s, ctx, 'gen1', 1), false);
assert.equal(buyMaterial(s, ctx), false);
assert.equal(buyMarketing(s, ctx), false);
s.funds = 100;
const quote = generatorQuote(s, ctx, 'gen1', 5);
assert.ok(buyGenerator(s, ctx, 'gen1', 5));
assert.equal(s.generators.gen1, 5);
assert.equal(s.funds, 100 - quote.cost);
});
test('"max" buys as many as affordable', () => {
const s = fresh();
s.funds = 1000;
buyGenerator(s, ctx, 'gen1', 'max');
assert.ok(s.generators.gen1 > 10);
assert.ok(s.funds >= 0);
});
test('upgrades need their unlock condition and requirement', () => {
const s = fresh();
s.funds = 1e9;
assert.equal(buyUpgrade(s, ctx, 'click_1'), false); // not unlocked yet
s.totalProduced = 25;
assert.ok(buyUpgrade(s, ctx, 'click_1'));
assert.equal(buyUpgrade(s, ctx, 'click_1'), false); // already owned
s.totalProduced = 1e6;
assert.equal(upgradeVisible(s, ctx.upgradesById.get('mat_eff_2')), false); // requires mat_eff_1
buyUpgrade(s, ctx, 'mat_eff_1');
assert.ok(upgradeVisible(s, ctx.upgradesById.get('mat_eff_2')));
});
test('upgrade effects reach the derived values', () => {
const s = fresh();
s.upgrades = ['click_1', 'demand_1', 'mat_eff_1', 'auto_buy'];
const d = derive(s, ctx);
assert.equal(d.clickYield, 2);
assert.equal(d.demandMultiplier, 1.5);
assert.equal(d.materialPerUnit, 1 / 1.25);
assert.equal(d.autoBuyMaterial, true);
});
test('auto-buy restocks material when funds allow', () => {
const s = fresh();
s.upgrades = ['auto_buy'];
s.material = 10;
s.funds = 100;
advance(s, ctx, 1);
assert.ok(s.material > 50);
});
test('offline progress is capped, discounted, and ignored when short', () => {
const s = fresh();
s.generators.gen1 = 5;
s.material = 1e6;
assert.equal(simulateOffline(s, ctx, 3), null);
const report = simulateOffline(s, ctx, 10 * 24 * 3600);
assert.equal(report.seconds, ctx.rules.offline.maxSeconds);
assert.ok(report.produced > 0);
});
test('sanitizeState rejects junk and clamps values', () => {
assert.equal(sanitizeState('nope', ctx), null);
assert.equal(sanitizeState([], ctx), null);
const s = sanitizeState(
{
units: -5,
funds: 'lots',
price: 9999,
marketing: 1e9,
generators: { gen1: 3.7, ghost: 5, gen2: -2 },
upgrades: ['click_1', 'click_1', 'nonsense'],
savedAt: Number.POSITIVE_INFINITY,
},
ctx,
5_000,
);
assert.equal(s.units, 0);
assert.equal(s.funds, 0);
assert.equal(s.price, ctx.rules.market.maxPrice);
assert.ok(s.marketing <= 300);
assert.deepEqual(s.generators, { gen1: 3, gen2: 0, gen3: 0, gen4: 0, gen5: 0, gen6: 0 });
assert.deepEqual(s.upgrades, ['click_1']);
assert.equal(s.savedAt, 5_000);
});
test('state survives a serialize / sanitize round trip', () => {
const s = fresh();
s.funds = 123.456;
s.generators.gen2 = 4;
s.upgrades = ['click_1', 'g1_a'];
s.marketing = 3;
const back = sanitizeState(JSON.parse(serializeState(s)), ctx);
assert.equal(back.funds, 123.456);
assert.deepEqual(back.generators, s.generators);
assert.deepEqual(back.upgrades, s.upgrades);
assert.equal(back.marketing, 3);
});
test('the full save of a late game state is small enough for cookies', () => {
const s = fresh();
s.funds = 123456789.123456;
for (const g of ctx.generators) s.generators[g.id] = 2500;
s.upgrades = ctx.upgrades.map((u) => u.id);
assert.ok(serializeState(s).length < 1500);
});