feat: New theme + wire price adjustement
This commit is contained in:
@@ -138,6 +138,59 @@ test('auto-buy restocks material when funds allow', () => {
|
||||
assert.ok(s.material > 50);
|
||||
});
|
||||
|
||||
test('material is costly at first and cheaper relative to the sale price as production grows', () => {
|
||||
const { fairPrice } = ctx.rules.market;
|
||||
const costShare = (totalProduced) => {
|
||||
const s = fresh();
|
||||
s.totalProduced = totalProduced;
|
||||
const d = derive(s, ctx);
|
||||
return d.batchCost / d.batchSize / fairPrice;
|
||||
};
|
||||
assert.ok(costShare(0) > 0.25, 'expensive at the start');
|
||||
assert.ok(costShare(1e4) < costShare(0));
|
||||
assert.ok(costShare(1e6) < costShare(1e4));
|
||||
assert.ok(costShare(1e9) < 0.1, 'a small share of revenue late in the game');
|
||||
});
|
||||
|
||||
test('material lots grow with the operation, in doublings', () => {
|
||||
const sizeAt = (total) => {
|
||||
const s = fresh();
|
||||
s.totalProduced = total;
|
||||
return derive(s, ctx).batchSize;
|
||||
};
|
||||
assert.equal(sizeAt(0), ctx.rules.materialMarket.minBatch);
|
||||
assert.ok(sizeAt(1e6) > sizeAt(1e4));
|
||||
assert.equal(Math.log2(sizeAt(1e6) / ctx.rules.materialMarket.minBatch) % 1, 0);
|
||||
});
|
||||
|
||||
test('the material market is erratic but stays within its bounds', () => {
|
||||
const m = ctx.rules.materialMarket;
|
||||
const s = fresh();
|
||||
let seed = 7;
|
||||
const rng = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
||||
const seen = [];
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
advance(s, ctx, m.updateEverySeconds, rng);
|
||||
seen.push(s.materialFactor);
|
||||
}
|
||||
assert.ok(Math.min(...seen) >= m.minFactor && Math.max(...seen) <= m.maxFactor);
|
||||
assert.ok(Math.max(...seen) > 1.6 && Math.min(...seen) < 0.6, 'swings widely');
|
||||
assert.ok(new Set(seen.map((f) => f.toFixed(2))).size > 100, 'does not sit still');
|
||||
});
|
||||
|
||||
test('auto-buy waits for a fair price unless stock is nearly gone', () => {
|
||||
const s = fresh();
|
||||
s.upgrades = ['auto_buy'];
|
||||
s.funds = 1e6;
|
||||
s.materialFactor = 2.5; // expensive
|
||||
s.material = 40; // low but not desperate (< lot/2, > lot/10)
|
||||
advance(s, ctx, 0.1, () => 0.5);
|
||||
assert.equal(s.material, 40, 'did not buy at a high price');
|
||||
s.material = 5; // desperate
|
||||
advance(s, ctx, 0.1, () => 0.5);
|
||||
assert.ok(s.material > 50, 'buys when nearly out');
|
||||
});
|
||||
|
||||
test('offline progress is capped, discounted, and ignored when short', () => {
|
||||
const s = fresh();
|
||||
s.generators.gen1 = 5;
|
||||
|
||||
+17
-14
@@ -6,27 +6,33 @@ import { decode, encode } from '../public/js/services/storage.js';
|
||||
import { loadContext } from './helpers.js';
|
||||
|
||||
const ctx = loadContext();
|
||||
const themes = ['paperclip', 'witch'];
|
||||
|
||||
test('export then import restores the save and its theme', () => {
|
||||
test('export then import restores the save', () => {
|
||||
const state = createState(ctx);
|
||||
state.funds = 42;
|
||||
state.generators.gen1 = 7;
|
||||
const text = buildExport('witch', state);
|
||||
const imported = parseImport(text, ctx, themes);
|
||||
assert.equal(imported.themeId, 'witch');
|
||||
assert.equal(imported.state.funds, 42);
|
||||
assert.equal(imported.state.generators.gen1, 7);
|
||||
const imported = parseImport(buildExport(state), ctx);
|
||||
assert.equal(imported.funds, 42);
|
||||
assert.equal(imported.generators.gen1, 7);
|
||||
});
|
||||
|
||||
test('the export does not depend on the theme', () => {
|
||||
assert.equal('theme' in JSON.parse(buildExport(createState(ctx))), false);
|
||||
});
|
||||
|
||||
test('first-version exports (with a theme field) still import', () => {
|
||||
const legacy = { ...JSON.parse(buildExport(createState(ctx))), theme: 'witch' };
|
||||
assert.equal(parseImport(JSON.stringify(legacy), ctx).units, 0);
|
||||
});
|
||||
|
||||
test('import also accepts text with a BOM and surrounding whitespace (.txt files)', () => {
|
||||
const text = ` \n${buildExport('paperclip', createState(ctx))}\n\n`;
|
||||
assert.equal(parseImport(text, ctx, themes).themeId, 'paperclip');
|
||||
const text = ` \n${buildExport(createState(ctx))}\n\n`;
|
||||
assert.ok(parseImport(text, ctx));
|
||||
});
|
||||
|
||||
const failure = (text) => {
|
||||
try {
|
||||
parseImport(text, ctx, themes);
|
||||
parseImport(text, ctx);
|
||||
} catch (error) {
|
||||
assert.ok(error instanceof ImportError);
|
||||
return error.code;
|
||||
@@ -38,10 +44,7 @@ test('import rejects bad input with a specific code', () => {
|
||||
assert.equal(failure(''), 'empty');
|
||||
assert.equal(failure('not json'), 'invalid');
|
||||
assert.equal(failure('{"hello":1}'), 'wrong_game');
|
||||
const unknown = JSON.parse(buildExport('paperclip', createState(ctx)));
|
||||
unknown.theme = 'dragons';
|
||||
assert.equal(failure(JSON.stringify(unknown)), 'unknown_theme');
|
||||
const broken = JSON.parse(buildExport('paperclip', createState(ctx)));
|
||||
const broken = JSON.parse(buildExport(createState(ctx)));
|
||||
broken.state = 'x';
|
||||
assert.equal(failure(JSON.stringify(broken)), 'invalid');
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ test('resolveSafe stays inside the public directory', () => {
|
||||
test('/api/themes lists installed themes with their languages', async () => {
|
||||
const themes = await handleApi(PUBLIC_DIR, '/api/themes');
|
||||
const ids = themes.map((t) => t.id);
|
||||
assert.ok(ids.includes('paperclip') && ids.includes('witch') && ids.includes('alien'));
|
||||
for (const id of ['paperclip', 'witch', 'alien', 'insects']) assert.ok(ids.includes(id), id);
|
||||
assert.deepEqual(themes.find((t) => t.id === 'paperclip').locales, ['en', 'fr']);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user