feat: news ledger with lane-delayed price snapshots
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { GOODS, NEWS_DAYS_PER_LANE, SNAPSHOT_KEEP } from './constants.js';
|
||||
import { advancePlanet, priceOf } from './economy.js';
|
||||
|
||||
// Daily price snapshots per planet. Knowledge from `hops` lanes away
|
||||
// is the snapshot from hops * NEWS_DAYS_PER_LANE days ago.
|
||||
export class NewsLedger {
|
||||
constructor() {
|
||||
this.history = new Map(); // planetId -> [{time, prices}]
|
||||
}
|
||||
|
||||
record(galaxy, t) {
|
||||
for (const planet of galaxy.planets) {
|
||||
advancePlanet(planet, t);
|
||||
const prices = Object.fromEntries(GOODS.map((g) => [g, priceOf(g, planet.stock[g])]));
|
||||
let arr = this.history.get(planet.id);
|
||||
if (!arr) this.history.set(planet.id, (arr = []));
|
||||
arr.push({ time: t, prices });
|
||||
if (arr.length > SNAPSHOT_KEEP) arr.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Latest snapshot of planetId that has had time to travel `hops` lanes by time t.
|
||||
// Clamped to t=0: the initial state is universally known.
|
||||
knownPrices(planetId, hops, t) {
|
||||
const arr = this.history.get(planetId);
|
||||
if (!arr || arr.length === 0) return null;
|
||||
const asOf = Math.max(0, t - hops * NEWS_DAYS_PER_LANE);
|
||||
let best = null;
|
||||
for (const snap of arr) {
|
||||
if (snap.time <= asOf) best = snap;
|
||||
else break;
|
||||
}
|
||||
return best ?? arr[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { NewsLedger } from '../src/sim/news.js';
|
||||
import { generateGalaxy } from '../src/sim/galaxy.js';
|
||||
import { priceOf } from '../src/sim/economy.js';
|
||||
|
||||
describe('NewsLedger', () => {
|
||||
it('records snapshots and serves them with lane delay', () => {
|
||||
const galaxy = generateGalaxy('news-test', { systemCount: 8 });
|
||||
const news = new NewsLedger();
|
||||
news.record(galaxy, 0);
|
||||
news.record(galaxy, 1);
|
||||
news.record(galaxy, 2);
|
||||
|
||||
const pid = galaxy.planets[0].id;
|
||||
// 0 hops away at t=2: freshest snapshot (t=2)
|
||||
expect(news.knownPrices(pid, 0, 2).time).toBe(2);
|
||||
// 2 hops away at t=2: only the t=0 snapshot has reached us
|
||||
expect(news.knownPrices(pid, 2, 2).time).toBe(0);
|
||||
// far away early on: clamps to the universally-known t=0 snapshot
|
||||
expect(news.knownPrices(pid, 5, 1).time).toBe(0);
|
||||
});
|
||||
|
||||
it('snapshot prices match the live price curve at record time', () => {
|
||||
const galaxy = generateGalaxy('news-test', { systemCount: 8 });
|
||||
const news = new NewsLedger();
|
||||
news.record(galaxy, 0);
|
||||
const p = galaxy.planets[0];
|
||||
const snap = news.knownPrices(p.id, 0, 0);
|
||||
expect(snap.prices.ore).toBeCloseTo(priceOf('ore', p.stock.ore), 5);
|
||||
});
|
||||
|
||||
it('trims history beyond SNAPSHOT_KEEP entries', () => {
|
||||
const galaxy = generateGalaxy('news-test', { systemCount: 8 });
|
||||
const news = new NewsLedger();
|
||||
for (let t = 0; t <= 50; t++) news.record(galaxy, t);
|
||||
// a 45-day-old snapshot is gone; we get the oldest retained one instead
|
||||
const snap = news.knownPrices(galaxy.planets[0].id, 45, 50);
|
||||
expect(snap.time).toBeGreaterThanOrEqual(50 - 30);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user