// Corporate Ladder's own tuning. // // What is true of this pack because it is *this* pack: the shape of its week, // when wages and rent land, how the loan behaves, when Dispatch opens and how // deep the Trevor chain is. Everything true of a pack because it is a pack — // validity, dead content, the economic baseline, variety, replay — is in // conformance.test.js, which runs over every pack in the registry. // // Engine behaviour is covered against the fixture pack instead, so tuning the // game cannot break the engine suite and vice versa. // // One trap, learned the hard way: the chooser's RNG must be seeded // *differently* from the game's. Both draw one number per turn from the same // generator, so sharing a seed correlates the choice index with the event // draw and silently makes whole option combinations unreachable — which looks // exactly like a content bug. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { corporateLadder as pack } from '../content/corporateladder/index.js'; import { startGame, currentTurn, takeTurn } from '../src/engine/game.js'; import { play, sweep, median } from './fixtures/strategies.js'; test('the promotion is worth taking, and reachable by a deliberate player', () => { const { byStrategy } = sweep(pack, { seeds: 12, turns: 200 }); const promoted = Object.values(byStrategy).flat().filter((s) => s.stage === 'dispatch'); const stayed = Object.values(byStrategy).flat().filter((s) => s.stage === 'mailroom'); assert.ok(promoted.length > 0 && stayed.length > 0, 'both outcomes should occur'); assert.ok(median(promoted.map((s) => s.money)) > median(stayed.map((s) => s.money)), 'the coordinator wage should leave a player better off'); // A player actually pursuing it should get there mid-run, not at the very // end — the mailroom is a career, not a tutorial. const timings = []; for (let seed = 0; seed < 12; seed++) { let at = null; play(pack, { strategy: 'sociable', seed, turns: 200, visit: ({ state }) => { if (state.stage === 'dispatch' && at === null) at = state.turn; } }); if (at) timings.push(at); } assert.equal(timings.length, 12, 'a sociable player should always get the offer'); const typical = median(timings); assert.ok(typical > 40 && typical < 90, `promotion landed at turn ${typical}`); }); test('hard times is uncommon for a careful player and real for a careless one', () => { const countHardTimes = (strategy) => { let days = 0; play(pack, { strategy, seed: 'ht', turns: 200, visit: ({ turn }) => { if (turn.event.id === 'rent_bounced') days++; } }); return days; }; assert.equal(countHardTimes('thrifty'), 0, 'minding the money should keep you out of trouble'); assert.ok(countHardTimes('spendthrift') > 0, 'ignoring it should land you in it'); }); test('money arrives and leaves on a rhythm the player can plan around', () => { const wages = []; const rent = []; let state = startGame(pack, { seed: 'rhythm' }); for (let t = 0; t < 30; t++) { const turn = currentTurn(pack, state); const result = takeTurn(pack, state, turn.options.find((o) => o.available).option.id); for (const change of result.record.upkeep) { if (change.label === 'Wages') wages.push(state.turn); if (change.label === 'Rent') rent.push(state.turn); } state = result.state; } assert.deepEqual(wages, [5, 19], 'paid on alternate Fridays'); assert.deepEqual(rent, [8, 15, 22, 29], 'rent every Monday from the second week'); }); test('the calendar names the day and counts the week', () => { let state = startGame(pack, { seed: 'calendar' }); assert.deepEqual( [state.calendar.name, state.calendar.cycle, state.calendar.index], ['Monday', 1, 0], ); for (let t = 0; t < 11; t++) { const turn = currentTurn(pack, state); state = takeTurn(pack, state, turn.options.find((o) => o.available).option.id).state; } assert.equal(state.turn, 12); assert.deepEqual([state.calendar.name, state.calendar.cycle], ['Friday', 2]); }); test('debt cannot spiral without limit: lending stops and the charge escalates', () => { let peakDebt = 0; for (const strategy of ['spendthrift', 'desperate', 'random']) { for (let seed = 0; seed < 12; seed++) { play(pack, { strategy, seed, turns: 200, visit: ({ state }) => { peakDebt = Math.max(peakDebt, state.debt); } }); } } // The loan is gated at debt < 2000 and lends 500, so 2500 plus stalled // interest is the ceiling. Well above that means the gate has stopped working. assert.ok(peakDebt <= 3000, `debt reached ${peakDebt}; the borrowing ceiling is not holding`); const charges = pack.upkeep.filter((u) => u.label?.startsWith('Loan service charge')); assert.ok(charges.length >= 3, 'the service charge should step up as debt grows'); }); test('a deliberate player can reach relationship-gated content in a reasonable run', () => { // The Trevor chain is the deepest gate in the pack. A player cultivating him // should get there well inside a normal session, not at turn 100. const regard = (o) => (o.option.effects ?? []) .filter((e) => e.path === 'relationships.trevor.likes_you') .reduce((sum, e) => sum + e.value, 0); let state = startGame(pack, { seed: 'befriend-trevor' }); let reached = null; for (let t = 0; t < 60 && reached === null; t++) { const turn = currentTurn(pack, state); if (turn.event.id === 'trevor_favor') { reached = state.turn; break; } const best = turn.options.filter((o) => o.available).sort((a, b) => regard(b) - regard(a))[0]; state = takeTurn(pack, state, best.option.id).state; } assert.ok(reached && reached < 45, `Trevor's chain opened at turn ${reached ?? 'never'}`); });