Files
TheLadder/test/effects.test.js
T
JesseMarkowitzandClaude Opus 5 112e558dbb Fix repayments that overcharged, and act on the first Frontier playtest
A debt repayment took its whole instalment even when less was owed: four
dollars owed at the store cost twenty. Content could not express "pay what is
owed", so an add effect's amount may now be read from state, with a cap. Five
repayments across both packs use it, and a conformance test holds every pack to
it. The turn record now says which options were hidden, so the analyser stops
counting doors nobody saw, and it reads only feedback logs.

From the same session: wages are labelled as wages, a clamped change says where
it stopped, two unreadable option labels are rewritten, the sale barn no longer
offers what the player already owns, Ward hints at what the promotion waits for,
and the office has five more events. DECISIONS #38-40.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6UDQ9o6L6Ey173U7XVou6
2026-09-15 10:23:15 -04:00

106 lines
4.3 KiB
JavaScript

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