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
286 lines
12 KiB
JavaScript
286 lines
12 KiB
JavaScript
// The Frontier — the second content pack for The Ladder.
|
||
//
|
||
// Cold Fork, a freight town at the end of a stage line. You are a hired hand
|
||
// in Vane & Co.'s yard; the town has a marshal who is tired and a road that is
|
||
// not safe. Two rungs, the same as Corporate Ladder, and not one line of
|
||
// engine code between them.
|
||
//
|
||
// This pack exists to prove the split holds. Everything that makes it a
|
||
// different game from Corporate Ladder is in this directory: a four-day cycle
|
||
// instead of a week, money that arrives from work rather than from a wage,
|
||
// relationship stats named for a place where being owed a favour matters more
|
||
// than being liked, and a skill pair of nerve and trade.
|
||
|
||
import { characters } from './characters.js';
|
||
import { events } from './events/index.js';
|
||
|
||
export const frontier = {
|
||
id: 'frontier',
|
||
name: 'The Frontier',
|
||
blurb:
|
||
'Cold Fork, at the end of a stage line: a freight yard, a road that is ' +
|
||
'not safe, and a marshal who has been six years retiring.',
|
||
version: '0.1.1',
|
||
turnUnit: 'day',
|
||
startingStage: 'hand',
|
||
peopleHeading: 'Cold Fork',
|
||
|
||
stages: {
|
||
hand: { label: 'The Yard', title: 'Hired Hand' },
|
||
deputy: { label: 'The Office', title: 'Deputy Marshal' },
|
||
},
|
||
|
||
// Not a week. The stage comes every fourth day, and the whole town runs on
|
||
// that: the outfit settles up when the stage comes in, the boarding house
|
||
// collects on the rest day before it. A pack that wants a seven-day week
|
||
// says seven here; the engine has no opinion either way, and Corporate
|
||
// Ladder's calendar is the same field with different numbers in it.
|
||
calendar: {
|
||
cycleLength: 4,
|
||
cycleName: 'Run',
|
||
unitNames: ['Stage day', 'Haul', 'Yard', 'Rest'],
|
||
},
|
||
|
||
// Money is smaller here than in Corporate Ladder by a factor of about
|
||
// fifteen, and it behaves differently: the settlement covers board and room
|
||
// and very little else, so the difference between a good run and a bad one
|
||
// is what you took from the work. See `tuning` below for the arithmetic that
|
||
// is actually asserted.
|
||
state: {
|
||
'money': { initial: 40, min: -400, max: 100000 },
|
||
// The store ledger. Named for what it is rather than for a loan: nobody in
|
||
// Cold Fork lends money, they let you carry a balance.
|
||
'owed': { initial: 0, min: 0, max: 100000 },
|
||
'standing': { initial: 50, min: 0, max: 100 },
|
||
'skills.nerve': { initial: 5, min: 0, max: 100 },
|
||
'skills.trade': { initial: 5, min: 0, max: 100 },
|
||
// The same shape as Corporate Ladder's lifestyle, in this setting's own
|
||
// terms and at this setting's own scale: what the office expects a man to
|
||
// turn up on, and what keeping that up costs. See DECISIONS #36.
|
||
'lifestyle': { initial: 0, min: 0, max: 2 },
|
||
'flags.carries_iron': { initial: false },
|
||
'flags.owes_the_store': { initial: false },
|
||
'flags.covered_for_dutch': { initial: false },
|
||
'flags.sworn_in': { initial: false },
|
||
'flags.spoke_for_dutch': { initial: false },
|
||
'notable': { initial: [] },
|
||
},
|
||
|
||
// A different vocabulary from Corporate Ladder's likes/fears/helps, and the
|
||
// reason the vocabulary is content's to choose. Out here the useful question
|
||
// about a person is not whether they like you: it is whether they will take
|
||
// your word, and whether they are carrying a debt to you they mean to pay.
|
||
relationshipStats: {
|
||
trusts_you: { label: 'Takes your word', initial: 45, min: 0, max: 100 },
|
||
owes_you: { label: 'Owes you', initial: 10, min: 0, max: 100 },
|
||
wary_of_you: { label: 'Wary of you', initial: 15, min: 0, max: 100 },
|
||
},
|
||
|
||
// The economic shape, and the thing that makes this pack a real test of the
|
||
// engine rather than Corporate Ladder in a hat: there is no wage worth
|
||
// living on. Vane settles up when the stage comes in and it covers the room
|
||
// and the feed and four dollars besides. Everything else you earn, you earn
|
||
// on a turn, by choosing to — and what the choices cost is where the
|
||
// pressure comes from, not an unexplained drain.
|
||
//
|
||
// Keep this arithmetic honest when tuning. Per four-day run:
|
||
// hand: -12 board -8 room +24 settlement = +4
|
||
// deputy: -12 board -8 room +38 pay = +18
|
||
// Both are asserted directly by the conformance suite against `tuning`
|
||
// below, so a number changed here without re-deriving it fails the build
|
||
// rather than trapping a player forty turns in.
|
||
upkeep: [
|
||
{ label: 'Board and feed', effects: [{ path: 'money', op: 'add', value: -3 }] },
|
||
// Turn 1 is a Stage day, so offset 3 is the first Rest day and offset 4
|
||
// the next Stage day — the room is collected the night before the outfit
|
||
// settles up, which is the boarding house's whole business model.
|
||
{
|
||
label: 'Room at the Cordwood',
|
||
every: 4,
|
||
offset: 3,
|
||
effects: [{ path: 'money', op: 'add', value: -8 }],
|
||
},
|
||
// Labelled as wages, because they are. "Settlement" read as jargon to the
|
||
// first real player, who could not tell it was the twenty-four a run the
|
||
// first morning quotes; the ledger is written for a player (DECISIONS #27).
|
||
{
|
||
label: 'Wages from Vane',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'stage', op: '==', value: 'hand' }],
|
||
effects: [{ path: 'money', op: 'add', value: 24 }],
|
||
},
|
||
{
|
||
label: 'Wages from the county',
|
||
every: 4,
|
||
offset: 4,
|
||
// The town pays a real wage, and it is still not much of a living. What
|
||
// changes with the star is not the money; it is what the days contain.
|
||
requires: [{ path: 'stage', op: '==', value: 'deputy' }],
|
||
effects: [{ path: 'money', op: 'add', value: 38 }],
|
||
},
|
||
// What the star is expected to arrive on, and what that expectation costs
|
||
// to meet. The office nets +18 a run; a deputy on his own horse is +11,
|
||
// and one with the horse and the room is +4 and makes the rest on the
|
||
// days. Money is meant to keep mattering after the promotion — but the
|
||
// first pass charged 9 for the room and left a keeper's median balance at
|
||
// $8 after 200 turns, which is not pressure, it is the debt trap this pack
|
||
// was tuned twice to avoid.
|
||
{
|
||
label: 'Feed and shoeing',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'lifestyle', op: '>=', value: 1 }],
|
||
effects: [{ path: 'money', op: 'add', value: -7 }],
|
||
},
|
||
{
|
||
label: 'The room over the Ellis place',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'lifestyle', op: '>=', value: 2 }],
|
||
effects: [{ path: 'money', op: 'add', value: -7 }],
|
||
},
|
||
// Gated on the office for the same reason Corporate Ladder gates its
|
||
// equivalent on Dispatch: a hand cannot afford any of this, and being
|
||
// charged standing for that would be a trap rather than a choice.
|
||
{
|
||
label: 'What the star is expected to be',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [
|
||
{ path: 'stage', op: '==', value: 'deputy' },
|
||
{ path: 'lifestyle', op: '<', value: 1 },
|
||
],
|
||
effects: [{ path: 'standing', op: 'add', value: -2 }],
|
||
},
|
||
{
|
||
label: 'What the star is expected to be',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [
|
||
{ path: 'stage', op: '==', value: 'deputy' },
|
||
{ path: 'standing', op: '>=', value: 65 },
|
||
{ path: 'lifestyle', op: '<', value: 2 },
|
||
],
|
||
effects: [{ path: 'standing', op: 'add', value: -2 }],
|
||
},
|
||
{
|
||
// The top step, for the reason Corporate Ladder's has one: without it a
|
||
// man who plays for standing every single day holds a hundred of it on a
|
||
// livery horse, and the bay is money thrown away. With it he settles in
|
||
// the low eighties and the last stretch has to be bought.
|
||
label: 'What the star is expected to be',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [
|
||
{ path: 'stage', op: '==', value: 'deputy' },
|
||
{ path: 'standing', op: '>=', value: 80 },
|
||
{ path: 'lifestyle', op: '<', value: 2 },
|
||
],
|
||
effects: [{ path: 'standing', op: 'add', value: -14 }],
|
||
},
|
||
{
|
||
// The charge that never stops, for the reason Corporate Ladder has one:
|
||
// without it a man who has bought everything and stands well with the
|
||
// whole town has nothing left that money is for. Being somebody in Cold
|
||
// Fork costs, every run, and nothing you can buy ends it.
|
||
label: 'What the standing costs',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [
|
||
{ path: 'stage', op: '==', value: 'deputy' },
|
||
{ path: 'standing', op: '>=', value: 85 },
|
||
],
|
||
// Four rather than six: this pack's late game is already flat — measured,
|
||
// a deputy who has bought everything drifts 0 a run either way — so the
|
||
// charge is here to keep money live, not to arrest an accumulation that
|
||
// does not happen. Six left an ordinary run at 55; four leaves 105.
|
||
effects: [{ path: 'money', op: 'add', value: -4 }],
|
||
},
|
||
// Store credit costs more the deeper in you are, the same step function
|
||
// Corporate Ladder uses for its loan — a shape, not a setting.
|
||
{
|
||
label: 'Store credit',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'owed', op: '>', value: 0 }],
|
||
effects: [{ path: 'money', op: 'add', value: -3 }],
|
||
},
|
||
{
|
||
label: 'Store credit (carried)',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'owed', op: '>=', value: 60 }],
|
||
effects: [{ path: 'money', op: 'add', value: -3 }],
|
||
},
|
||
{
|
||
label: 'Store credit (posted)',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'owed', op: '>=', value: 120 }],
|
||
effects: [{ path: 'money', op: 'add', value: -4 }],
|
||
},
|
||
],
|
||
|
||
// What the conformance suite holds this pack to. Declaring it here rather
|
||
// than writing it into a test is the same principle as the rest of the pack:
|
||
// the numbers a setting must hit are the setting's business. See
|
||
// DECISIONS.md #31.
|
||
tuning: {
|
||
// Net movement of `money` from upkeep alone, per calendar cycle, at each
|
||
// stage. Upkeep is the floor a player plays against: barely above water in
|
||
// the yard, and a real if unremarkable living with the star.
|
||
baselinePerCycle: {
|
||
hand: [0, 12],
|
||
deputy: [10, 30],
|
||
},
|
||
// States a player opts into or earns, excluded from the baseline above:
|
||
// the floor is what a man faces owing nothing, keeping nothing, and being
|
||
// nobody in particular.
|
||
baselineExcludes: ['owed', 'lifestyle', 'standing'],
|
||
},
|
||
|
||
display: {
|
||
'money': { label: 'Money', format: 'money', order: 1 },
|
||
'owed': { label: 'Owed at the store', format: 'money', order: 2, hideWhenZero: true },
|
||
'standing': { label: 'Standing', format: 'meter', order: 3 },
|
||
'skills.nerve': { label: 'Nerve', format: 'meter', order: 4 },
|
||
'skills.trade': { label: 'Trade', format: 'meter', order: 5 },
|
||
'lifestyle': {
|
||
label: 'What you keep', format: 'tier', order: 6, hideWhenZero: true,
|
||
labels: ['The livery horse', 'Your own horse', 'Horse and a room'],
|
||
},
|
||
'notable': { hidden: true, inLedger: false },
|
||
'flags.carries_iron': {
|
||
label: 'The scattergun', hidden: true,
|
||
whenTrue: 'You are carrying Vane’s scattergun on the box.',
|
||
whenFalse: 'The scattergun is back under the office counter.',
|
||
},
|
||
'flags.owes_the_store': {
|
||
label: 'The store ledger', hidden: true,
|
||
whenTrue: 'Your name is in the book at the store.',
|
||
whenFalse: 'You are square with the store.',
|
||
},
|
||
'flags.covered_for_dutch': {
|
||
label: 'Dutch’s run', hidden: true,
|
||
whenTrue: 'Dutch owes you a run.',
|
||
whenFalse: 'You and Dutch are square.',
|
||
},
|
||
'flags.sworn_in': {
|
||
label: 'The star', hidden: true,
|
||
whenTrue: 'You are sworn in.',
|
||
whenFalse: 'You are back in the yard.',
|
||
},
|
||
'flags.spoke_for_dutch': {
|
||
label: 'Dutch’s name', hidden: true,
|
||
whenTrue: 'Dutch’s name is in for the good runs.',
|
||
whenFalse: 'Dutch’s name is not in for the good runs.',
|
||
},
|
||
},
|
||
|
||
characters,
|
||
events,
|
||
};
|
||
|
||
export default frontier;
|