Files
TheLadder/content/corporateladder/events/routine.js
T
JesseMarkowitzandClaude Opus 5 e482f07157 Add the interface, then rebuild the economy around the first playtest
The MVP is playable: a turn screen, a career retrospective, an optional
feedback widget, local-storage autosave with file export/import, and a build
that flattens everything into one self-contained HTML file.

A turn has two phases — the scenario and its options, then what the choice did
and, separately, what the day cost regardless. Numbers that move without the
player seeing why are most of what makes them meaningless. Locked options are
shown greyed with the requirement spelled out rather than hidden, so a player
can see the door they cannot open yet.

The interface is named entirely by the pack. `pack.display` maps a state path to
a label, a format and an order, which is why the header reads "Standing" for a
path called `reputation` and why debt disappears while it is zero.

The first playtest then found three things, all of them fair.

*"Wasn't clear what daily drain was for."* The ledger was labelling changes by
the path that moved — "Money −$18" — when the upkeep entry that caused them had
a perfectly good name. It now says "Coffee, transit, lunch". The labels were in
the data the whole time and never reached the screen.

*"Should probably pay rent weekly / get paid every other week… would be good to
have day of week and week # shown."* Packs can now declare a calendar, and the
engine derives the day name, cycle and index from the turn number before the
event is drawn — so content can require a Friday and the header can say
"Thursday · Week 2 · Day 12". Upkeep entries take `every` and `offset`, making
wages fall on alternate Fridays and rent every Monday. The turn screen carries
a diary line — "Wages tomorrow · Rent in 4 days" — computed generically from
whatever a pack schedules.

*"Always in debt — could never get ahead."* The old economy bled $25 a day
regardless of play. It is now roughly break-even at baseline, with a
`spare_shift` event that appears *because* you are broke: the way out of a hole
should be visible from inside it. Across archetypes a careful player ends around
$1,400, an unplanned one treads water, and a careless one sinks into real debt.

That retune broke the coverage test, usefully. Random play stopped reaching hard
times at all, which made the entire debt branch look dead — it was not, since
random play is not a plausible player. Coverage is now the union across five
archetypes, including one that is broke *and* well-liked, because the "ask a
friend for money" options sit in a state no single-axis strategy reaches.

Not covered by any of this: styling and dark mode, which only a human with a
browser can check. This machine has none that can render a page.

109 tests. Reasoning in docs/DECISIONS.md §14-21.

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

437 lines
15 KiB
JavaScript

// Ordinary days in the mailroom.
//
// The first three carry no conditions, no cooldown and no `once`, which makes
// them the stage's floor: whatever else is excluded, there is always a turn to
// play. Everything else in the pack leans on that.
export const routine = [
{
id: 'first_day',
stage: 'mailroom',
weight: 99999,
once: true,
description:
'Marlene Hobb hands you a laminated card with the org chart on it. Your ' +
'name is not on the card. She says it will be, eventually, in pen.',
options: [
{
id: 'listen',
label: 'Ask her to walk you through it.',
result: 'She walks you through it. Then she walks you through it again.',
effects: [
{ path: 'skills.negotiation', op: 'add', value: 2 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 4 },
{ path: 'notable', op: 'push', value: 'Let Marlene explain the org chart. Twice.' },
],
},
{
id: 'wing_it',
label: "Say you'll pick it up as you go.",
result: 'She puts the card back in her pocket without comment.',
effects: [
{ path: 'skills.logistics', op: 'add', value: 2 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: -2 },
],
},
{
id: 'ask_trevor',
label: 'Ask Trevor where you actually sit.',
result:
'He shows you a desk, then mentions he has been here two weeks longer ' +
'than you. This will not be the last time.',
effects: [
{ path: 'relationships.trevor.likes_you', op: 'add', value: 3 },
{ path: 'relationships.trevor.wants_to_help_you', op: 'add', value: 2 },
],
},
],
},
{
id: 'mail_cart',
stage: 'mailroom',
weight: 100,
description:
"The mail cart's left front wheel has developed a shriek. Facilities has " +
'classified it as "cosmetic."',
options: [
{
id: 'push',
label: "Push it anyway. Everyone will know you're coming.",
result: 'By Thursday, two floors can time their morning by you.',
effects: [
{ path: 'skills.logistics', op: 'add', value: 1 },
{ path: 'reputation', op: 'add', value: -2 },
],
},
{
id: 'requisition',
label: 'Spend the morning on a requisition form.',
result:
'The form requires the cart\'s asset number. The asset number is on the ' +
'wheel. You fill it in from underneath.',
effects: [
{ path: 'skills.negotiation', op: 'add', value: 2 },
{ path: 'skills.logistics', op: 'add', value: 1 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 1 },
],
},
{
id: 'swap',
label: 'Swap it with the good cart from the third floor.',
result: 'The third floor has no way of proving anything. They try anyway.',
effects: [
{ path: 'skills.logistics', op: 'add', value: 3 },
{ path: 'reputation', op: 'add', value: -3 },
{ path: 'relationships.trevor.fears_you', op: 'add', value: 3 },
],
},
],
},
{
id: 'sorting_backlog',
stage: 'mailroom',
weight: 100,
description:
'Two bins came in overnight and neither is labelled. Trevor has decided ' +
'this is a good day to reorganise his desk.',
options: [
{
id: 'grind',
label: 'Sort both bins yourself.',
result: 'It takes until four. Marlene notices, which is the point.',
effects: [
{ path: 'skills.logistics', op: 'add', value: 3 },
{ path: 'reputation', op: 'add', value: 2 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 3 },
// Trevor's bin got sorted too, and he is not too proud to notice.
{ path: 'relationships.trevor.likes_you', op: 'add', value: 2 },
],
},
{
id: 'split',
label: 'Ask Trevor to take one.',
requires: [{ path: 'skills.negotiation', op: '>=', value: 15 }],
result: 'He takes one. He takes the lighter one, but he takes one.',
effects: [
{ path: 'skills.negotiation', op: 'add', value: 2 },
{ path: 'skills.logistics', op: 'add', value: 1 },
{ path: 'relationships.trevor.likes_you', op: 'add', value: -2 },
],
},
{
id: 'halfway',
label: 'Sort what fits before lunch and leave the rest.',
result: 'The rest is still there on Monday, and so are you.',
effects: [
{ path: 'reputation', op: 'add', value: -2 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: -2 },
],
},
],
},
{
id: 'franking_machine',
stage: 'mailroom',
weight: 70,
description:
'The franking machine is making a noise it has not made before. The manual ' +
'is in a drawer that is locked, and the key is a subject nobody will be ' +
'drawn on.',
options: [
{
id: 'fix',
label: 'Take the panel off and have a look.',
requires: [{ path: 'skills.logistics', op: '>=', value: 20 }],
result: 'It was a spring. It is now a spring in the right place.',
effects: [
{ path: 'skills.logistics', op: 'add', value: 3 },
{ path: 'reputation', op: 'add', value: 4 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 3 },
{
path: 'notable',
op: 'push',
value: 'Fixed the franking machine, which nobody had managed since 2019.',
},
],
},
{
id: 'cycle',
label: 'Turn it off and on.',
result: 'The noise stops. Nothing has been learned by anyone.',
effects: [{ path: 'skills.logistics', op: 'add', value: 1 }],
},
{
id: 'escalate',
label: 'Log it with Facilities and wait.',
result: 'Facilities classifies it as cosmetic.',
effects: [
{ path: 'skills.negotiation', op: 'add', value: 1 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: -1 },
],
},
],
},
{
id: 'birthday_card',
stage: 'mailroom',
weight: 45,
cooldown: 5,
description:
'A card is going round for someone on the fourth floor. You have never met ' +
'them. The envelope already has two twenties in it and your name is next ' +
'on the list.',
options: [
{
id: 'contribute',
label: 'Put something in.',
result: 'You are now a person who puts something in.',
effects: [
{ path: 'money', op: 'add', value: -20 },
{ path: 'reputation', op: 'add', value: 3 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 2 },
{ path: 'relationships.trevor.likes_you', op: 'add', value: 2 },
],
},
{
id: 'sign_only',
label: 'Sign it and pass it on.',
result: 'You write "all the best" and immediately regret the phrasing.',
effects: [{ path: 'reputation', op: 'add', value: -1 }],
},
{
id: 'pass',
label: 'Pass it on unsigned.',
result: 'Trevor checks the list. Trevor notices the list.',
effects: [
{ path: 'reputation', op: 'add', value: -3 },
{ path: 'relationships.trevor.likes_you', op: 'add', value: -2 },
],
},
],
},
{
id: 'elevator',
stage: 'mailroom',
weight: 30,
cooldown: 10,
description:
'You are alone in the elevator with a man whose name is on the quarterly. ' +
'He looks at your cart. He asks how long the mail takes to get to eleven.',
options: [
{
id: 'honest',
label: 'Tell him the truth: forty minutes, on a good day.',
result: 'He says "forty" once, to himself, and gets out on nine.',
effects: [
{ path: 'reputation', op: 'add', value: 5 },
{ path: 'skills.negotiation', op: 'add', value: 2 },
{
path: 'notable',
op: 'push',
value: 'Told an executive the truth about the eleventh floor.',
},
],
},
{
id: 'optimistic',
label: 'Tell him twenty.',
result: 'He seems pleased. Somebody, eventually, will have to make it twenty.',
effects: [
{ path: 'reputation', op: 'add', value: -3 },
{ path: 'skills.negotiation', op: 'add', value: 1 },
],
},
{
id: 'unsure',
label: "Say you're not sure.",
result: 'The rest of the ride is four floors long.',
effects: [{ path: 'reputation', op: 'add', value: -1 }],
},
],
},
{
id: 'overtime_week',
stage: 'mailroom',
weight: 40,
cooldown: 7,
description:
'There is a weekend inventory. It pays. Marlene mentions it in the tone of ' +
'someone who has been told to mention it.',
options: [
{
id: 'take',
label: 'Take the shift.',
result: 'Two days of counting things that were already counted.',
effects: [
{ path: 'money', op: 'add', value: 180 },
{ path: 'skills.logistics', op: 'add', value: 2 },
{ path: 'reputation', op: 'add', value: 2 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 2 },
],
},
{
id: 'negotiate',
label: 'Take it, for a bit more.',
requires: [{ path: 'skills.negotiation', op: '>=', value: 30 }],
result:
'Marlene does not enjoy the conversation, but she does have the ' +
'authority, and you both find that out at the same time.',
effects: [
{ path: 'money', op: 'add', value: 260 },
{ path: 'skills.negotiation', op: 'add', value: 3 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: -1 },
{
path: 'notable',
op: 'push',
value: 'Negotiated a better rate for the weekend inventory.',
},
],
},
{
id: 'decline',
label: 'Pass.',
result: 'You have a weekend. It is the only one anyone gets.',
effects: [],
},
],
},
{
id: 'promotion_rumor',
stage: 'mailroom',
weight: 25,
cooldown: 12,
requires: [{ path: 'turn', op: '>=', value: 30 }],
description:
'There is a post going in Dispatch. Trevor has heard about it. Trevor ' +
'mentions that he has heard about it.',
options: [
{
id: 'ask_marlene',
label: 'Ask Marlene to put your name forward.',
requires: [{ path: 'relationships.marlene.wants_to_help_you', op: '>=', value: 60 }],
result: 'She says she will see. From Marlene this is close to a contract.',
effects: [
{ path: 'reputation', op: 'add', value: 5 },
{ path: 'relationships.marlene.wants_to_help_you', op: 'add', value: -5 },
{
path: 'notable',
op: 'push',
value: 'Asked Marlene to put your name forward for Dispatch.',
},
],
},
{
id: 'ask_around',
label: 'Ask around on your own.',
result: 'You learn the post has been open for eleven months.',
effects: [
{ path: 'skills.negotiation', op: 'add', value: 3 },
{ path: 'reputation', op: 'add', value: 2 },
],
},
{
id: 'ignore',
label: 'Stay where you are for now.',
result: 'Trevor mentions it again on Thursday.',
effects: [],
},
],
},
{
id: 'late_run',
stage: 'mailroom',
weight: 35,
cooldown: 5,
description:
'The evening courier run needs covering. It is an hour, it is nobody\'s ' +
'favourite hour, and it pays.',
options: [
{
id: 'take_run',
label: 'Take the run.',
result: 'An hour in the loading bay. The city looks better from there than it has any right to.',
effects: [
{ path: 'money', op: 'add', value: 70 },
{ path: 'skills.logistics', op: 'add', value: 1 },
],
},
{
id: 'swap_it',
label: 'Take it, and trade it to Trevor for the Friday one.',
requires: [{ path: 'skills.negotiation', op: '>=', value: 15 }],
result: 'Friday pays the same and finishes earlier. Trevor works this out on Friday.',
effects: [
{ path: 'money', op: 'add', value: 70 },
{ path: 'skills.negotiation', op: 'add', value: 2 },
{ path: 'relationships.trevor.likes_you', op: 'add', value: -3 },
{ path: 'relationships.trevor.fears_you', op: 'add', value: 2 },
],
},
{
id: 'go_home',
label: 'Go home.',
result: 'You are home by six. It is a strange feeling and it does not last.',
effects: [{ path: 'relationships.marlene.likes_you', op: 'add', value: -1 }],
},
],
},
{
// Deliberately surfaced by being broke: when the money runs low the board
// starts offering work. The way out of a hole should be visible from
// inside it, not something you had to have planned for.
id: 'spare_shift',
stage: 'mailroom',
weight: 300,
cooldown: 2,
requires: [{ path: 'money', op: '<', value: 350 }],
description:
'There is a shift going spare on the board — someone else\'s weekend, ' +
'going cheap because nobody wants it. Marlene has not taken it down.',
options: [
{
id: 'take_shift',
label: 'Take it.',
result: 'Two days of somebody else\'s work. The money is real.',
effects: [
{ path: 'money', op: 'add', value: 140 },
{ path: 'skills.logistics', op: 'add', value: 1 },
{ path: 'relationships.marlene.likes_you', op: 'add', value: 1 },
],
},
{
id: 'take_two',
label: 'Take it, and ask what else is going.',
requires: [{ path: 'skills.negotiation', op: '>=', value: 15 }],
result:
'There is more going than the board admits to. Marlene finds you ' +
'another two days without writing either of them down.',
effects: [
{ path: 'money', op: 'add', value: 265 },
{ path: 'skills.negotiation', op: 'add', value: 2 },
{ path: 'reputation', op: 'add', value: -1 },
],
},
{
id: 'leave_it',
label: 'Leave it for someone who needs it.',
result: 'Trevor takes it. Trevor mentions taking it.',
effects: [
{ path: 'reputation', op: 'add', value: 2 },
{ path: 'relationships.trevor.likes_you', op: 'add', value: 2 },
],
},
],
},
];
export default routine;