Add chance and a cast that changes between runs, and a pack to try them
An option used to do exactly what it said. Now it may carry a check: one of
four attributes (Nerve, Wits, Charm, Resolve) against a set difficulty or
against a named person's own attribute, leaning on how they regard you, with a
success and a failure. The player sees only the odds in words ("a long shot",
"likely"), never a number. The roll is one draw from the run's generator, so a
run still replays exactly from its seed and choices.
A character may be a role with several authored candidates, one drawn per run,
or a group of two to four drawn into numbered slots. Everyone carries a name, a
description and the same four attributes as the player, so a replayed pack
meets a different office. Text names people as {partner} or {clerk}, and an
event about "a clerk" binds one present clerk for the turn. The draw and the
binding are saved with the run, and a save from before a pack drew a cast gets
the same one every time it is loaded.
All of it is generic and optional: Corporate Ladder and the Frontier use none
of it and play as before. The Counting House, a one-stage office about 1910
built from the story-to-pack catalogue, exercises it, and was tuned against the
conformance suite by measurement. DECISIONS #41. Version 0.4.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MhABSrqkwTdcybXmheDPr
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
112e558dbb
commit
3108319a2b
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
// A small pack for the cast and checks (DECISIONS #41): a drawn single role, a
|
||||
// group role of two to four, a fixed character with attributes, opposed and
|
||||
// unopposed checks, and an event about "one of the co-workers". Its checks run
|
||||
// from 0 to 100 so tests can force an outcome; a real pack keeps a floor and a
|
||||
// ceiling so nothing is ever certain.
|
||||
|
||||
export const castPack = {
|
||||
id: 'castpack',
|
||||
name: 'Cast Pack',
|
||||
version: '1.0.0',
|
||||
turnUnit: 'day',
|
||||
startingStage: 'desk',
|
||||
|
||||
attributes: {
|
||||
nerve: { label: 'Nerve' },
|
||||
wits: { label: 'Wits' },
|
||||
charm: { label: 'Charm' },
|
||||
resolve: { label: 'Resolve' },
|
||||
},
|
||||
|
||||
checks: {
|
||||
base: 50, perPoint: 10, unopposed: 5,
|
||||
relationshipPerPoint: 0.5, relationshipCentre: 50,
|
||||
floor: 0, ceiling: 100,
|
||||
},
|
||||
|
||||
state: {
|
||||
'money': { initial: 100, min: -500, max: 10000 },
|
||||
'attributes.nerve': { initial: 5, min: 0, max: 10 },
|
||||
'attributes.wits': { initial: 5, min: 0, max: 10 },
|
||||
'attributes.charm': { initial: 5, min: 0, max: 10 },
|
||||
'attributes.resolve': { initial: 5, min: 0, max: 10 },
|
||||
},
|
||||
|
||||
relationshipStats: {
|
||||
trust: { label: 'Trusts you', initial: 50, min: 0, max: 100 },
|
||||
regard: { label: 'Thinks of you', initial: 50, min: 0, max: 100 },
|
||||
},
|
||||
|
||||
characters: [
|
||||
{
|
||||
id: 'boss',
|
||||
role_type: 'boss',
|
||||
title: 'Manager',
|
||||
candidates: [
|
||||
{ name: 'Agnes Pell', description: 'Reads every memo twice.', attributes: { nerve: 6, wits: 8, charm: 3, resolve: 7 } },
|
||||
{ name: 'Walter Grose', description: 'Loud, and quicker than he looks.', attributes: { nerve: 7, wits: 6, charm: 6, resolve: 4 },
|
||||
startingRelationship: { trust: 30 } },
|
||||
{ name: 'Mina Hart', description: 'Has never raised her voice.', attributes: { nerve: 4, wits: 7, charm: 8, resolve: 6 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'coworker',
|
||||
role_type: 'peer',
|
||||
title: 'Clerk',
|
||||
count: [2, 4],
|
||||
candidates: [
|
||||
{ name: 'Ben Aske', description: 'Owes {boss} a favour.', attributes: { nerve: 3, wits: 4, charm: 6, resolve: 5 } },
|
||||
{ name: 'Cora Dunn', description: 'First in, last out.', attributes: { nerve: 5, wits: 5, charm: 4, resolve: 8 } },
|
||||
{ name: 'Del Fane', description: 'Knows everyone\'s business.', attributes: { nerve: 4, wits: 7, charm: 5, resolve: 3 } },
|
||||
{ name: 'Edie Gow', description: 'Quiet until she is not.', attributes: { nerve: 7, wits: 5, charm: 3, resolve: 6 } },
|
||||
{ name: 'Fitz Hale', description: 'Charming, unreliable.', attributes: { nerve: 5, wits: 3, charm: 8, resolve: 2 } },
|
||||
],
|
||||
},
|
||||
{ id: 'porter', name: 'Old Sam', role_type: 'staff', description: 'Has seen everyone come and go.',
|
||||
attributes: { nerve: 2, wits: 9, charm: 4, resolve: 9 } },
|
||||
],
|
||||
|
||||
events: [
|
||||
{
|
||||
id: 'routine',
|
||||
stage: 'desk',
|
||||
weight: 100,
|
||||
description: 'A day at the desk under {boss}.',
|
||||
options: [
|
||||
{ id: 'work', label: 'Work.', effects: [{ path: 'money', op: 'add', value: 10 }] },
|
||||
{
|
||||
id: 'ask_for_more',
|
||||
label: 'Ask {boss.first} for more money.',
|
||||
check: { attribute: 'charm', against: 'boss', opposedBy: 'wits', relationship: 'trust' },
|
||||
effects: [{ path: 'relationships.boss.regard', op: 'add', value: -1 }],
|
||||
success: { result: '{boss} agrees.', effects: [{ path: 'money', op: 'add', value: 50 }] },
|
||||
failure: { result: '{boss} laughs.', effects: [{ path: 'relationships.boss.trust', op: 'add', value: -5 }] },
|
||||
},
|
||||
{
|
||||
id: 'hard_sum',
|
||||
label: 'Try the hard sum.',
|
||||
check: { attribute: 'wits', difficulty: 5 },
|
||||
success: { result: 'It adds up.', effects: [{ path: 'attributes.wits', op: 'add', value: 1 }] },
|
||||
failure: { result: 'It does not.', effects: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'needled',
|
||||
stage: 'desk',
|
||||
weight: 100,
|
||||
involves: ['coworker'],
|
||||
description: '{coworker} makes a remark about your tie.',
|
||||
options: [
|
||||
{ id: 'ignore', label: 'Let it go.', effects: [{ path: 'relationships.{coworker}.regard', op: 'add', value: -2 }] },
|
||||
{
|
||||
id: 'retort',
|
||||
label: 'Answer {coworker.first} back.',
|
||||
check: { attribute: 'nerve', against: '{coworker}', relationship: 'regard' },
|
||||
success: { result: '{coworker} laughs, and means it.', effects: [{ path: 'relationships.{coworker}.regard', op: 'add', value: 5 }] },
|
||||
failure: { result: 'It lands badly.', effects: [{ path: 'relationships.{coworker}.trust', op: 'add', value: -5 }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default castPack;
|
||||
Vendored
+24
-7
@@ -24,13 +24,30 @@ const amountOf = (effect) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const moneyGain = (resolved) => (resolved.option.effects ?? [])
|
||||
.filter((e) => e.path === 'money' && e.op === 'add')
|
||||
.reduce((sum, e) => sum + amountOf(e), 0);
|
||||
/**
|
||||
* Every effect an option can have, with the weight a player reading the odds
|
||||
* would give it: plain effects count in full, a check's success and failure by
|
||||
* their chance (DECISIONS #41). An option with no check is just its effects.
|
||||
*/
|
||||
export const weightedEffects = (resolved) => {
|
||||
const { option, odds } = resolved;
|
||||
const plain = (option.effects ?? []).map((effect) => ({ effect, weight: 1 }));
|
||||
if (!option.check) return plain;
|
||||
const p = (odds?.chance ?? 50) / 100;
|
||||
return [
|
||||
...plain,
|
||||
...(option.success?.effects ?? []).map((effect) => ({ effect, weight: p })),
|
||||
...(option.failure?.effects ?? []).map((effect) => ({ effect, weight: 1 - p })),
|
||||
];
|
||||
};
|
||||
|
||||
const relationshipGain = (resolved) => (resolved.option.effects ?? [])
|
||||
.filter((e) => e.path.startsWith('relationships.'))
|
||||
.reduce((sum, e) => sum + amountOf(e), 0);
|
||||
const moneyGain = (resolved) => weightedEffects(resolved)
|
||||
.filter(({ effect }) => effect.path === 'money' && effect.op === 'add')
|
||||
.reduce((sum, { effect, weight }) => sum + amountOf(effect) * weight, 0);
|
||||
|
||||
const relationshipGain = (resolved) => weightedEffects(resolved)
|
||||
.filter(({ effect }) => effect.path.startsWith('relationships.'))
|
||||
.reduce((sum, { effect, weight }) => sum + amountOf(effect) * weight, 0);
|
||||
|
||||
const best = (options, score) => [...options].sort((a, b) => score(b) - score(a))[0];
|
||||
|
||||
@@ -59,7 +76,7 @@ export const strategies = {
|
||||
*/
|
||||
lifer: (options) => {
|
||||
const stays = options.filter(
|
||||
(o) => !(o.option.effects ?? []).some((e) => e.path === 'stage'),
|
||||
(o) => !weightedEffects(o).some(({ effect }) => effect.path === 'stage'),
|
||||
);
|
||||
return best(stays.length ? stays : options, relationshipGain);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user