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); } });