import { test } from 'node:test'; import assert from 'node:assert/strict'; import { applyEffect, applyEffects } from '../src/engine/effects.js'; const schema = { 'money': { initial: 0, min: -500, max: 1000 }, 'skills.talking': { initial: 1, min: 0, max: 10 }, 'flags.in_debt': { initial: false }, 'notable': { initial: [] }, }; const state = { money: 100, skills: { talking: 1 }, flags: { in_debt: false }, notable: [] }; test('add and set produce a new state', () => { const result = applyEffects(state, [ { path: 'money', op: 'add', value: -30 }, { path: 'flags.in_debt', op: 'set', value: true }, ], schema); assert.equal(result.state.money, 70); assert.equal(result.state.flags.in_debt, true); assert.equal(state.money, 100, 'the input state is never mutated'); }); test('push appends to a list', () => { const result = applyEffects(state, [{ path: 'notable', op: 'push', value: 'first day' }], schema); assert.deepEqual(result.state.notable, ['first day']); assert.deepEqual(state.notable, [], 'the input list is never mutated'); }); test('remove drops matching entries', () => { const seeded = { ...state, notable: ['a', 'b', 'a'] }; const result = applyEffects(seeded, [{ path: 'notable', op: 'remove', value: 'a' }], schema); assert.deepEqual(result.state.notable, ['b']); }); test('values clamp to their declared bounds', () => { const high = applyEffect(state, { path: 'skills.talking', op: 'add', value: 99 }, schema); assert.equal(high.state.skills.talking, 10); assert.equal(high.change.clamped, true); const low = applyEffect(state, { path: 'money', op: 'add', value: -9999 }, schema); assert.equal(low.state.money, -500); }); test('an unbounded path is not clamped', () => { const result = applyEffect(state, { path: 'money', op: 'add', value: 10 }, {}); assert.equal(result.state.money, 110); assert.equal(result.change.clamped, false); }); test('the change record carries before and after for every path touched', () => { const result = applyEffects(state, [ { path: 'money', op: 'add', value: 25 }, { path: 'skills.talking', op: 'add', value: 1 }, ], schema); assert.deepEqual(result.changes.map((c) => [c.path, c.from, c.to]), [ ['money', 100, 125], ['skills.talking', 1, 2], ]); }); test('adding to a path that has never been set treats it as zero', () => { const result = applyEffect({}, { path: 'money', op: 'add', value: 5 }, {}); assert.equal(result.state.money, 5); }); test('an unknown operation is a loud failure', () => { assert.throws(() => applyEffect(state, { path: 'money', op: 'multiply', value: 2 }), /unknown effect operation/); }); test('an amount can be read from state, capped and signed', () => { // The Frontier's store, turn 27 of a real session: four owed, twenty taken. const ledger = { owed: { initial: 0, min: 0, max: 1000 } }; const pay = [ { path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 20 } }, { path: 'owed', op: 'add', value: { from: 'owed', times: -1, cap: 20 } }, ]; const small = applyEffects({ money: 110, owed: 4 }, pay, ledger); assert.equal(small.state.money, 106, 'pays what is owed, not the instalment'); assert.equal(small.state.owed, 0); assert.equal(small.changes[1].clamped, false, 'nothing is lost to a clamp'); assert.equal(small.changes[0].value, -4, 'the change records the amount applied'); assert.deepEqual(small.changes[0].source, { from: 'owed', times: -1, cap: 20 }); const large = applyEffects({ money: 110, owed: 50 }, pay, ledger); assert.equal(large.state.money, 90, 'the cap holds when more is owed'); assert.equal(large.state.owed, 30); }); test('an amount read from a path that is not a number is zero', () => { const result = applyEffect({ money: 10 }, { path: 'money', op: 'add', value: { from: 'owed', times: -1 } }, {}); assert.equal(result.state.money, 10); assert.equal(result.change.value === 0 || Object.is(result.change.value, -0), true); }); test('a literal amount records no source', () => { const result = applyEffect(state, { path: 'money', op: 'add', value: 5 }, schema); assert.equal('source' in result.change, false); }); test('an empty effect list is a no-op', () => { const result = applyEffects(state, [], schema); assert.deepEqual(result.changes, []); assert.equal(result.state, state); });