import assert from 'node:assert/strict'; import { test } from 'node:test'; import { evaluateFormula, buildFormulaContext } from './formula'; import { computeDerived, sortDerived, CycleError } from './derived'; import { validatePointBuy, validateArray, rollDice, sum, validateCreationRules } from './creation'; import { snapshotHash, canonicalJson } from './snapshot'; import { validateSystemDefinition } from './systemValidate'; import type { SystemDefinition } from './types'; import { FORMAT_VERSION, SYSTEM_FORMAT } from './types'; test('evaluateFormula: D&D ability modifier', () => { const ctx = { 'attr.str': 16 }; assert.equal(evaluateFormula('floor((attr.str - 10) / 2)', ctx), 3); assert.equal(evaluateFormula('(attr.str - 10) / 2', ctx, 'floor'), 3); }); test('evaluateFormula: clamp min max', () => { assert.equal(evaluateFormula('clamp(attr.str, 1, 20)', { 'attr.str': 25 }), 20); }); test('computeDerived topo order', () => { const derived = computeDerived( [ { key: 'strMod', label: 'STR mod', formula: 'floor((attr.str - 10) / 2)', dependsOn: ['attr.str'], round: 'none', }, { key: 'carry', label: 'Carry', formula: 'attr.str * 15', dependsOn: ['attr.str'], }, ], { attributes: { str: 12 } }, ); assert.equal(derived.strMod, 1); assert.equal(derived.carry, 180); }); test('sortDerived detects cycles', () => { assert.throws( () => sortDerived([ { key: 'a', label: 'a', formula: 'derived.b', dependsOn: ['derived.b'] }, { key: 'b', label: 'b', formula: 'derived.a', dependsOn: ['derived.a'] }, ]), CycleError, ); }); test('point buy budget', () => { const rule = { id: 'pb', type: 'point_buy' as const, appliesTo: 'attribute' as const, params: { budget: 27, keys: ['str', 'dex', 'con', 'int', 'wis', 'cha'], }, }; const ok = validatePointBuy(rule, { str: 15, dex: 14, con: 13, int: 12, wis: 10, cha: 8 }); assert.equal(ok.length, 0); const bad = validatePointBuy(rule, { str: 15, dex: 15, con: 15, int: 15, wis: 15, cha: 15 }); assert.ok(bad.some((e) => e.code === 'point_buy_budget')); }); test('array permutation', () => { const rule = { id: 'arr', type: 'array' as const, appliesTo: 'attribute' as const, params: { array: [15, 14, 13, 12, 10, 8], keys: ['str', 'dex', 'con', 'int', 'wis', 'cha'] }, }; assert.equal( validateArray(rule, { str: 15, dex: 14, con: 13, int: 12, wis: 10, cha: 8 }).length, 0, ); assert.ok(validateArray(rule, { str: 15, dex: 15, con: 13, int: 12, wis: 10, cha: 8 }).length > 0); }); test('rollDice 4d6dl', () => { let i = 0; const seq = [0.0, 0.9, 0.5, 0.2]; // -> 1,6,4,2 drop 1 => 6+4+2=12 const rolls = rollDice('4d6dl', () => seq[i++] ?? 0); assert.equal(sum(rolls), 12); }); test('snapshotHash stable', () => { const system = minimalSystem(); const a = snapshotHash(system); const b = snapshotHash({ ...system, name: 'Other display name' }); // name is not part of hash payload — hash equal assert.equal(a, b); const c = snapshotHash({ ...system, revision: 2 }); assert.notEqual(a, c); }); test('canonicalJson sorts keys', () => { assert.equal(canonicalJson({ b: 1, a: 2 }), '{"a":2,"b":1}'); }); test('validateSystemDefinition', () => { const r = validateSystemDefinition(minimalSystem()); assert.equal(r.ok, true); }); test('validateCreationRules integrates point_buy', () => { const system = minimalSystem(); const result = validateCreationRules( system, { attributes: { str: 15, dex: 14, con: 13, int: 12, wis: 10, cha: 8 } }, 'point_buy', ); assert.equal(result.ok, true); }); test('buildFormulaContext', () => { const ctx = buildFormulaContext({ attributes: { str: 10 }, resources: { hp: 8 } }); assert.equal(ctx['attr.str'], 10); assert.equal(ctx['resource.hp'], 8); }); function minimalSystem(): SystemDefinition { return { format: SYSTEM_FORMAT, formatVersion: FORMAT_VERSION, id: 'test', name: 'Test', schemaVersion: 1, revision: 1, bricks: { attributes: [ { key: 'str', label: 'STR', scale: 'numeric', min: 8, max: 15, default: 10 }, { key: 'dex', label: 'DEX', scale: 'numeric', min: 8, max: 15, default: 10 }, { key: 'con', label: 'CON', scale: 'numeric', min: 8, max: 15, default: 10 }, { key: 'int', label: 'INT', scale: 'numeric', min: 8, max: 15, default: 10 }, { key: 'wis', label: 'WIS', scale: 'numeric', min: 8, max: 15, default: 10 }, { key: 'cha', label: 'CHA', scale: 'numeric', min: 8, max: 15, default: 10 }, ], derived: [ { key: 'strMod', label: 'STR Mod', formula: 'floor((attr.str - 10) / 2)', dependsOn: ['attr.str'], }, ], }, creationRules: [ { id: 'pb', type: 'point_buy', appliesTo: 'attribute', params: { budget: 27, keys: ['str', 'dex', 'con', 'int', 'wis', 'cha'], }, }, ], sheetLayout: { pageSize: 'fluid', grid: { size: 8 }, pages: [{ id: 'main', label: 'Main', widgets: [] }], }, }; }