feat: First iteration, multi themes, I18N
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user