import { test } from 'node:test'; import assert from 'node:assert/strict'; import { eligibleEvents, selectEvent, resolveOptions } from '../src/engine/select.js'; import { createState, resolveSchema } from '../src/engine/state.js'; import { createRng } from '../src/engine/rng.js'; import { setPath } from '../src/engine/paths.js'; import { testPack } from './fixtures/test-pack.js'; const schema = resolveSchema(testPack); const fresh = () => createState(testPack, { seed: 'select', schema }); test('a conditional event is out of the pool until its condition holds', () => { const rich = fresh(); assert.ok(!eligibleEvents(testPack, rich).eligible.some((e) => e.id === 'hard_times')); const broke = setPath(rich, 'money', -20); assert.ok(eligibleEvents(testPack, broke).eligible.some((e) => e.id === 'hard_times')); }); test('exclusions say why', () => { const { excluded } = eligibleEvents(testPack, fresh()); const hardTimes = excluded.find((x) => x.event.id === 'hard_times'); assert.equal(hardTimes.reason, 'requires'); assert.equal(hardTimes.failed[0].path, 'money'); }); test('a once-only event leaves the pool after it fires', () => { const state = setPath(fresh(), 'seen.first_day', 1); const { eligible, excluded } = eligibleEvents(testPack, state); assert.ok(!eligible.some((e) => e.id === 'first_day')); assert.equal(excluded.find((x) => x.event.id === 'first_day').reason, 'once'); }); test('a cooldown holds an event back for the declared number of turns', () => { let state = fresh(); state = setPath(state, 'lastSeen.audit', 5); state = setPath(state, 'turn', 9); assert.equal(eligibleEvents(testPack, state).excluded.find((x) => x.event.id === 'audit')?.reason, 'cooldown'); state = setPath(state, 'turn', 15); assert.ok(eligibleEvents(testPack, state).eligible.some((e) => e.id === 'audit')); }); test('events belonging to another stage are never drawn', () => { const elsewhere = setPath(fresh(), 'stage', 'penthouse'); const { eligible, excluded } = eligibleEvents(testPack, elsewhere); assert.equal(eligible.length, 0); assert.ok(excluded.every((x) => x.reason === 'stage')); }); test('selection is a weighted draw, so the heavy event dominates', () => { const rng = createRng('draw'); const state = setPath(fresh(), 'seen.first_day', 1); // take the opener out of the way const counts = {}; for (let i = 0; i < 1000; i++) { const event = selectEvent(testPack, state, rng); counts[event.id] = (counts[event.id] ?? 0) + 1; } assert.ok(counts.routine > 900, `routine (weight 100) should dominate audit (weight 5): ${JSON.stringify(counts)}`); assert.ok(counts.audit > 0, 'a low weight is still reachable'); }); test('an option below its skill gate resolves as locked, with the reason', () => { const event = testPack.events.find((e) => e.id === 'routine'); const resolved = resolveOptions(fresh(), event); const charm = resolved.find((r) => r.option.id === 'charm'); assert.equal(charm.available, false); assert.equal(charm.failed[0].path, 'skills.talking'); assert.equal(charm.hidden, false, 'locked options are shown greyed out unless the content says otherwise'); assert.equal(resolved.find((r) => r.option.id === 'work').available, true); }); test('an option unlocks once the gate is met', () => { const skilled = setPath(fresh(), 'skills.talking', 5); const event = testPack.events.find((e) => e.id === 'routine'); assert.equal(resolveOptions(skilled, event).find((r) => r.option.id === 'charm').available, true); });