import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveTileEffect, rollsNeededFor, DIE_SIZES } from './tileEffects.js'; import { board } from './board.js'; import rollTables from './rollTables.js'; const spaces = Object.values(board.spaces); const byType = (type) => spaces.filter((s) => s.type === type); test('DIE_SIZES covers every die actually referenced in the real board', () => { const usedDice = new Set(spaces.map((s) => s.die).filter(Boolean)); for (const die of usedDice) assert.ok(DIE_SIZES[die], `unknown die size: ${die}`); }); test('rollsNeededFor asks for exactly one roll per referenced table', () => { const multi = byType('roll_table_ref').find((s) => s.externalTables.length === 3); assert.ok(multi, 'expected a 3-table roll_table_ref tile in the real board'); assert.equal(rollsNeededFor(multi).length, 3); const single = byType('dice_space')[0]; assert.equal(rollsNeededFor(single).length, 1); const payday = byType('payday')[0]; assert.deepEqual(rollsNeededFor(payday), []); }); test('dice_space resolves against its real referenced table with an exact banded effect', () => { const space = byType('dice_space')[0]; const table = rollTables.tables.find((t) => t.source_doc_id === space.externalTables[0]); assert.ok(table, 'dice_space tile should reference a real table'); const [loStr] = table.entries[0].range.split('-'); const roll = Number(loStr); const result = resolveTileEffect(space, [roll]); assert.deepEqual(result.statDelta, table.entries[0].effect); assert.equal(result.todo, false, 'a real referenced table is not an invented placeholder'); }); test('multi-table roll_table_ref sums every referenced table\'s effect', () => { const multi = byType('roll_table_ref').find((s) => s.externalTables.length >= 2); const rolls = rollsNeededFor(multi).map(() => 1); // roll the minimum on every table const expected = {}; for (const tableId of multi.externalTables) { const table = rollTables.tables.find((t) => t.source_doc_id === tableId); for (const [stat, delta] of Object.entries(table.entries[0].effect)) { expected[stat] = (expected[stat] ?? 0) + delta; } } assert.deepEqual(resolveTileEffect(multi, rolls).statDelta, expected); }); test('stop applies the rolled D8 value directly to age, no table lookup', () => { const stop = byType('stop')[0]; assert.equal(stop.die, 'D8'); const result = resolveTileEffect(stop, [5]); assert.deepEqual(result.statDelta, { age: 5 }); assert.equal(result.todo, false, 'age += roll is a specified mechanic, not invented content'); }); test('choice tiles are a harmless no-op while no board-graph.json choices exist yet', () => { const choice = byType('choice')[0]; assert.equal(choice.choices, undefined); const result = resolveTileEffect(choice, []); assert.deepEqual(result.statDelta, {}); assert.equal(result.todo, true); }); test('inline_table tiles resolve against a synthesized placeholder, clearly marked todo', () => { const inline = byType('inline_table')[0]; assert.deepEqual(rollsNeededFor(inline), [inline.die]); const max = DIE_SIZES[inline.die]; const result = resolveTileEffect(inline, [max]); // top of the range assert.equal(result.todo, true); assert.equal(typeof result.statDelta.cash, 'number'); }); test('payday and named cash_bonus tiles use fixed placeholder amounts, no roll needed', () => { const payday = byType('payday')[0]; assert.deepEqual(rollsNeededFor(payday), []); const paydayResult = resolveTileEffect(payday, []); assert.ok(paydayResult.statDelta.cash > 0); assert.equal(paydayResult.todo, true); const hundredK = byType('cash_bonus').find((s) => s.label === '100K'); assert.deepEqual(resolveTileEffect(hundredK, []).statDelta, { cash: 100000 }); const tenK = byType('cash_bonus').find((s) => s.label === '10K'); assert.deepEqual(resolveTileEffect(tenK, []).statDelta, { cash: 10000 }); }); test('401k (a cash_bonus with a die but no table) falls into the synthesized placeholder path', () => { const four01k = byType('cash_bonus').find((s) => s.label === '401k'); assert.ok(four01k, 'expected a 401k cash_bonus tile'); assert.equal(four01k.die, 'D20'); assert.deepEqual(rollsNeededFor(four01k), ['D20']); const result = resolveTileEffect(four01k, [20]); assert.equal(result.todo, true); }); test('every real tile resolves without throwing across the full range of its die', () => { for (const space of spaces) { const dice = rollsNeededFor(space); for (const roll of [1, ...dice.map((d) => DIE_SIZES[d])]) { const rolls = dice.map(() => roll); assert.doesNotThrow(() => resolveTileEffect(space, rolls), `${space.id} (${space.type}) threw on roll=${roll}`); } } });