3 Commits
Author SHA1 Message Date
JesseMarkowitzandClaude Opus 5.5 3a4758172d story-to-pack: add the corpora and the catalogue v2/v3
Fourteen corpora split and classified: aesop, bierce, chekhov, holmes,
keefe, lawson, lorimer, maupassant, nobody, plaintales, poe, torchy,
wallingford and winesburg, each with its splitter and the hand-written
groups and pages; catalogue v2 and v3; and the shared splitters
gutenberg_chunks.py, se_split.py and se_build.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rwKTmug58sEsJ72AuWsEi
2026-10-07 06:05:51 -04:00
JesseMarkowitzandClaude Opus 5 e5617b86ba Replace clustering with a catalogue, and hand-write the references to judge it against
The pipeline's embed-and-cluster step is dead, and this commit holds both the
evidence for that and the step proposed to replace it.

Predicaments. Scenes are re-described as "what the person is up against", with
no names, jobs or places, then embedded and clustered (redescribe.py,
topic_words.py, topic_share.py). The pilot chose qwen3:14b over 3b by reading
both side by side. Two defects the pilot exposed are fixed: split.py missed
titles in quotes and a contents subtitle after a dash, so three stories had been
merged into their neighbours, and strip_names.py read New York place names as
people. The corrected corpus is probe/v2 (97 stories, 839 scenes);
carry_summaries.py reuses the 829 unchanged v1 summaries. Topic share fell from
20% to 13% at k=60, short of the pre-registered 10%.

Hand references. Three corpora were read scene by scene and written up by hand,
under the same prompt rules the local models get, as a baseline to judge them
against: O. Henry (probe/v2/claude, 839 scenes, 20 situations), Wharton's
Descent of Man (probe/wharton, 262 scenes, 16 groups) and Jacobs's The Lady of
the Barge (probe/jacobs, 157 scenes, 19 groups). Each has its own README and a
readable page. No inference was used for any of them.

Catalogue. probe/catalogue maps every hand group in the three references onto 36
situation entries, with an answer key per corpus and one recurrence rule applied
to all three. classify.py assigns a scene one entry or none, leave-one-corpus-
out; score.py checks it against the key, with a self-test on random labels.

Why clustering is out: hand-written predicaments, embedded and clustered exactly
as the model's were, agree with the hand grouping at ARI 0.05 — no better than
the 14B text's 0.07. Better rewriting cannot rescue it. Embeddings cannot even
shortlist: the hand label is the nearest entry 13% of the time and in the top 8
half the time.

The classification runs are not here. The dev and test runs are pre-registered
in probe/catalogue/README.md with the bar set beforehand, and are blocked on the
inference host, whose GPU has fallen off the PCIe bus three times. The 30-scene
partial output in out/ is not a result.

Review page. The situation review is now a browser page rather than JSON edited
by hand (review_page.py, review_page_logic.cjs with Node tests, format schema
v2). It has never been rendered in a real browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BygvsUXV9eU6oHkTCkKZ1
2026-09-20 17:19:42 -04:00
JesseMarkowitzandClaude Opus 5 fa3769d0fe Add story-to-pack research and a structured situation review
Research toward building a content pack from a story corpus, kept on its own
branch and independent of the game. Records the selection experiments against
blind labels, and settles selection as gate G2 followed by a human review:
review.py writes REVIEW.md and a review.json form, apply_review.py checks the
filled form and writes situations.json for the next stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6UDQ9o6L6Ey173U7XVou6
2026-09-15 06:59:35 -04:00
839 changed files with 464159 additions and 2954 deletions
+29 -27
View File
@@ -25,7 +25,7 @@ real browser.
| Event selection, option gating | done | | Event selection, option gating | done |
| Turn loop, per-turn upkeep | done | | Turn loop, per-turn upkeep | done |
| Content validator + reachability tests | done | | Content validator + reachability tests | done |
| Corporate Ladder pack — 32 events, two tiers | done | | Corporate Ladder pack — 31 events, two tiers | done |
| Turn screen, retrospective, feedback widget | done | | Turn screen, retrospective, feedback widget | done |
| Local-storage save, file export/import | done | | Local-storage save, file export/import | done |
| Single-file build | done | | Single-file build | done |
@@ -34,10 +34,10 @@ real browser.
| Repetition analytics (`npm run analyze`) | done | | Repetition analytics (`npm run analyze`) | done |
| Version and build shown on screen | done | | Version and build shown on screen | done |
| Keyboard play (Enter / Tab) | done | | Keyboard play (Enter / Tab) | done |
| Second content pack — The Frontier, 37 events, two tiers | done | | Second content pack — The Frontier, 29 events, two tiers | done |
| Setting picker, one save slot per pack | done | | Setting picker, one save slot per pack | done |
| Pack conformance suite (every pack, every check) | done | | Pack conformance suite (every pack, every check) | done |
| Played in a real browser | Corporate Ladder since 2026-09-09; The Frontier 2026-09-15 (112 turns, 0.3.3) | | Played in a real browser | Corporate Ladder yes, first playtest 2026-09-09; **The Frontier not yet** |
The first playtest confirmed the interface, saves and the exported log all work The first playtest confirmed the interface, saves and the exported log all work
in a real browser. Its findings — an unexplained daily drain, no sense of the in a real browser. Its findings — an unexplained daily drain, no sense of the
@@ -58,7 +58,7 @@ npm run serve # then open http://localhost:8080
The game plays from the keyboard: Enter takes the focused option, Enter again The game plays from the keyboard: Enter takes the focused option, Enter again
moves to the next day, and Tab / Shift+Tab pick a different option. The top of moves to the next day, and Tab / Shift+Tab pick a different option. The top of
every screen names the game, the pack, your current title, and the build — every screen names the game, the pack, your current title, and the build —
`0.3.4 · dev` when running from source, and the commit it came from when built. `0.3.0 · dev` when running from source, and the commit it came from when built.
A server is needed during development because browsers refuse ES module imports A server is needed during development because browsers refuse ES module imports
over `file://`. If port 8080 is taken, run `python3 -m http.server <port>` over `file://`. If port 8080 is taken, run `python3 -m http.server <port>`
@@ -74,7 +74,7 @@ That is one self-contained file with the stylesheet and every module inlined.
It plays by double-clicking it — no server, no network, nothing installed. It plays by double-clicking it — no server, no network, nothing installed.
```sh ```sh
npm test # 162 tests: engine, both packs, UI wiring, and the build npm test # 149 tests: engine, both packs, UI wiring, and the build
``` ```
Tests are in three layers. `test/conformance.test.js` runs over **every pack in Tests are in three layers. `test/conformance.test.js` runs over **every pack in
@@ -101,32 +101,34 @@ used only to run the tests, the single-file build, and the log analyser.
## Where things stand ## Where things stand
The MVP is complete and has survived four playtests, and a second setting exists The MVP is complete and has survived three playtests, and a second setting now
to prove the engine/content split holds. Adding it required **no engine change** exists to prove the engine/content split holds. Adding it required **no engine
— and three interface changes, two of which were latent defects a second pack change** — and three interface changes, two of which were latent defects a
made visible (a single global save slot, and the one place the UI still named a second pack made visible (a single global save slot, and the one place the UI
setting). See DECISIONS.md #34. still named a setting). See DECISIONS.md #34.
The Frontier had its first human session on 2026-09-15: 112 turns, promoted on
turn 44, both lifestyle purchases taken and the standing charge firing as
designed. What it found — an overcharging debt repayment in both packs, wages no
one could recognise, a sale barn offering what the player already owned, and an
office stage too thin — is what 0.3.4 fixes. The repayment fix needed one small
engine addition: an effect's amount may be read from state. See DECISIONS #38–40.
Open questions, in the order they are likely to matter: Open questions, in the order they are likely to matter:
- **0.3.4 has not been played.** Everything in it is asserted by tests and - **The Frontier has never been played by a human.** Its economy is asserted by
archetype runs, which is the evidence that was not enough before. In simulation the conformance suite and exercised by six archetypes, and that is exactly the
promoted Frontier runs now end with about twice the money they did ($164 against evidence that was not enough the last two times. It needs a session before its
$88) and less standing, and why is not yet isolated. The next Frontier session numbers are believed.
should watch whether money stops mattering at the top. See DECISIONS #40.
- **Dispatch is twelve events against the mailroom's eighteen.** It passes the - **Standing costs money to keep, and the Frontier's version is unplayed.**
variety guard, but it has had far less play than the mailroom. The Frontier's Each setting has a `lifestyle` tier bought on the second rung and charged for
office had the same thinness in a real session; Dispatch may too. every cycle after, a standing penalty for lagging behind what the job expects,
and a charge levied by standing itself that no purchase ever ends. Corporate
Ladder's has had one 96-turn session: both purchases were found and taken, and
the standing pressure worked, but the run arrived at the top with nothing left
to spend on — which is where the never-stopping charge came from. The Frontier
carries the same system at its own scale and has never been in front of anyone.
See DECISIONS #36.
- **Dispatch is eleven events against the mailroom's nineteen.** It passes the
variety guard, but it has had far less play than the mailroom. Waits on more
playtesting.
- **A third tier in either pack.** Two rungs is what both settings have. The - **A third tier in either pack.** Two rungs is what both settings have. The
split is now proven across settings; it is not proven across a long ladder. The split is now proven across settings; it is not proven across a long ladder.
Frontier has now been played once, so this is the designer's call to open. Waits until the Frontier has been played.
Settled, and not open: Corporate Ladder's negotiation gate stays as it is, and Settled, and not open: Corporate Ladder's negotiation gate stays as it is, and
the feedback widget stays as it is (DECISIONS #35). Money mattering late is the feedback widget stays as it is (DECISIONS #35). Money mattering late is
+6 -10
View File
@@ -98,17 +98,14 @@ export const hardTimes = [
'A man from the lender calls the mailroom phone. Marlene answers it. She ' + 'A man from the lender calls the mailroom phone. Marlene answers it. She ' +
'hands it to you without expression and then, notably, without leaving.', 'hands it to you without expression and then, notably, without leaving.',
options: [ options: [
// Both take what is owed up to their size, never more — the same defect
// a real Frontier session found at the store, present here unplayed.
// Money first, because both effects read `debt` before it moves. See #38.
{ {
id: 'pay_chunk', id: 'pay_chunk',
label: 'Pay a real chunk of it down, up to $350.', label: 'Pay a real chunk of it down.',
requires: [{ path: 'money', op: '>=', value: 350 }], requires: [{ path: 'money', op: '>=', value: 350 }],
result: 'He is audibly disappointed.', result: 'He is audibly disappointed.',
effects: [ effects: [
{ path: 'money', op: 'add', value: { from: 'debt', times: -1, cap: 350 } }, { path: 'money', op: 'add', value: -350 },
{ path: 'debt', op: 'add', value: { from: 'debt', times: -1, cap: 350 } }, { path: 'debt', op: 'add', value: -350 },
{ path: 'reputation', op: 'add', value: 1 }, { path: 'reputation', op: 'add', value: 1 },
], ],
}, },
@@ -116,11 +113,10 @@ export const hardTimes = [
id: 'pay_minimum', id: 'pay_minimum',
label: 'Pay what you can.', label: 'Pay what you can.',
requires: [{ path: 'money', op: '>=', value: 60 }], requires: [{ path: 'money', op: '>=', value: 60 }],
result: 'Up to fifty against the loan, and ten for the privilege of the call.', result: 'Sixty dollars, fifty of which is the loan.',
effects: [ effects: [
{ path: 'money', op: 'add', value: -10 }, { path: 'money', op: 'add', value: -60 },
{ path: 'money', op: 'add', value: { from: 'debt', times: -1, cap: 50 } }, { path: 'debt', op: 'add', value: -50 },
{ path: 'debt', op: 'add', value: { from: 'debt', times: -1, cap: 50 } },
], ],
}, },
{ {
+1 -1
View File
@@ -15,7 +15,7 @@ export const corporateLadder = {
blurb: blurb:
'A modern office and a mailroom, two people who have been here longer ' + 'A modern office and a mailroom, two people who have been here longer ' +
'than you, and a laminated org chart with your name not on it.', 'than you, and a laminated org chart with your name not on it.',
version: '0.1.1', version: '0.1.0',
turnUnit: 'day', turnUnit: 'day',
startingStage: 'mailroom', startingStage: 'mailroom',
// What the interface calls the panel of people. The UI used to hardcode // What the interface calls the panel of people. The UI used to hardcode
-122
View File
@@ -1,122 +0,0 @@
// Harrow & Pike's counting house, as it might be staffed.
//
// Nobody here is fixed. Each role has several people who could fill it, and a
// run draws one partner, one chief clerk and two to four clerks from them
// (DECISIONS #41), so a second career in the same pack meets a different
// office. Every candidate carries the same four attributes the player does,
// and a description that should agree with them: a partner written as sharp
// has the wits to see through a bluff, and one written as easily flattered
// does not.
//
// Descriptions may name another role in braces, filled from whoever was drawn
// into it, so a nephew can be the nephew of whichever partner this run has.
export const characters = [
{
id: 'partner',
role_type: 'boss',
title: 'Senior Partner',
candidates: [
{
name: 'Josiah Crane',
description:
'Built the house from one room and a borrowed desk. Remembers every figure ' +
'he has ever been shown, and forgives very few of them.',
attributes: { nerve: 7, wits: 8, charm: 3, resolve: 8 },
startingRelationship: { trust: 40 },
},
{
name: 'Harriet Voss',
description:
'Took over the firm when her husband died and has doubled it since. ' +
'Listens to everything and says about a fifth of it.',
attributes: { nerve: 6, wits: 9, charm: 5, resolve: 7 },
},
{
name: 'Lionel Ashby',
description:
'Inherited the chair and would rather be at his club. Easily flattered, ' +
'easily bored, and sharper than he lets anyone see.',
attributes: { nerve: 4, wits: 6, charm: 7, resolve: 3 },
startingRelationship: { regard: 55 },
},
],
},
{
id: 'chief',
role_type: 'supervisor',
title: 'Chief Clerk',
candidates: [
{
name: 'Silas Pruett',
description:
'Thirty years at the same high desk. Counts the pen nibs on Fridays and ' +
'knows to the penny what the stationery cupboard should hold.',
attributes: { nerve: 5, wits: 6, charm: 2, resolve: 9 },
},
{
name: 'Ada Kemble',
description:
'Runs the floor with a pencil behind her ear and a list, kept in the ' +
'same pencil, of everybody\'s lapses.',
attributes: { nerve: 6, wits: 7, charm: 4, resolve: 7 },
startingRelationship: { trust: 40 },
},
{
name: 'Horace Lumb',
description:
'Wanted your desk for somebody else\'s nephew and has not forgotten it. ' +
'Polite to your face, in a way that takes some effort.',
attributes: { nerve: 4, wits: 5, charm: 5, resolve: 6 },
startingRelationship: { regard: 35 },
},
],
},
{
id: 'clerk',
role_type: 'peer',
title: 'Clerk',
count: [2, 4],
candidates: [
{
name: 'Tom Bassett',
description: 'Cheerful, generous with other people\'s time, and a borrower of small sums he means to repay.',
attributes: { nerve: 5, wits: 4, charm: 7, resolve: 3 },
},
{
name: 'Nell Farrow',
description: 'The quickest pen on the floor and does not mind who knows it. Wants the next desk up, and soon.',
attributes: { nerve: 5, wits: 8, charm: 5, resolve: 6 },
startingRelationship: { regard: 40 },
},
{
name: 'Arthur Quill',
description: 'Timid, exact, and never wrong about a column of figures. Apologises to doors.',
attributes: { nerve: 2, wits: 7, charm: 3, resolve: 7 },
},
{
name: 'Joe Madden',
description: 'The loudest laugh in the building and most of the jokes, not all of them kind.',
attributes: { nerve: 7, wits: 4, charm: 6, resolve: 3 },
},
{
name: 'Lottie Swain',
description: 'Types the partner\'s letters and so knows everything a week early. Tells almost none of it.',
attributes: { nerve: 4, wits: 7, charm: 7, resolve: 5 },
},
{
name: 'Percy Dyer',
description: 'A nephew of {partner}\'s, sent to learn the business. Has so far learned where the kettle is.',
attributes: { nerve: 3, wits: 3, charm: 6, resolve: 2 },
startingRelationship: { trust: 40 },
},
{
name: 'Walt Rudge',
description: 'Twenty years a clerk and content with it. The one everyone asks, and the last to be thanked.',
attributes: { nerve: 6, wits: 6, charm: 4, resolve: 8 },
},
],
},
];
export default characters;
-882
View File
@@ -1,882 +0,0 @@
// A clerk's days at Harrow & Pike.
//
// Every situation here comes from the story-to-pack catalogue (tools/
// story-to-pack/probe/catalogue/v3), where it recurred across public-domain
// office fiction: Torchy's door, Keefe's pay talk, Pooter's cheeky juniors,
// Lorimer's hard boss. The id names the catalogue entry where one fits.
//
// This pack exists to try checks and a drawn cast (DECISIONS #41). Most options
// test one of the four attributes, against a named person or against a set
// difficulty, and say what happens either way; the player sees only the odds
// in words. Names are written as `{partner}`, `{chief}` and `{clerk}` and filled
// from whoever this run drew. An event about "a clerk" lists `clerk` in
// `involves`, and the engine picks one present clerk for the turn.
const moment = (text) => ({ path: 'notable', op: 'push', value: text });
export const events = [
{
id: 'first_day',
stage: 'desk',
weight: 1000,
once: true,
involves: ['clerk'],
description:
'Your first morning at Harrow & Pike. {chief} shows you a stool, a pen, a ' +
'blotter and the clock, in that order, and leaves you to draw your own ' +
'conclusions about which matters most.',
options: [
{
id: 'before_the_clock',
label: 'Be on the stool before the clock is.',
result: '{chief} notices, and writes something down. It may even be good.',
effects: [
{ path: 'relationships.chief.regard', op: 'add', value: 4 },
moment('Began at Harrow & Pike, early.'),
],
},
{
id: 'meet_the_floor',
label: 'Go round and introduce yourself to {clerk}.',
result: '{clerk} shakes your hand and tells you which of the pens leak.',
effects: [
{ path: 'relationships.{clerk}.regard', op: 'add', value: 5 },
moment('Began at Harrow & Pike, and met {clerk} first.'),
],
},
],
},
// The floor: the one event nothing can exclude.
{
id: 'the_ledgers',
stage: 'desk',
weight: 40,
description:
'The ledgers are the same as last week\'s, only longer. {chief} has left a ' +
'note on your blotter about the width of your margins.',
options: [
{
id: 'head_down',
label: 'Keep your head down and work.',
result: 'The day goes into the columns, as days here do.',
effects: [
{ path: 'standing', op: 'add', value: 1 },
{ path: 'relationships.chief.trust', op: 'add', value: 1 },
],
},
{
id: 'stay_late',
label: 'Stay on after six to finish the day book.',
result: 'Overtime is paid, at the rate the firm thinks overtime is worth.',
effects: [
{ path: 'money', op: 'add', value: 2 },
{ path: 'relationships.chief.regard', op: 'add', value: 1 },
],
},
{
id: 'ease_off',
label: 'Make the day last.',
result: 'Nobody notices, today.',
effects: [{ path: 'standing', op: 'add', value: -1 }],
},
],
},
{
id: 'ask_for_more',
stage: 'desk',
weight: 25,
cooldown: 12,
requires: [
{ path: 'turn', op: '>=', value: 10 },
{ path: 'rises', op: '<', value: 3 },
],
description:
'You have been at the desk long enough to know what the work is worth, and ' +
'your lodgings have not got any cheaper. {partner} is in, and alone.',
options: [
{
id: 'ask',
label: 'Knock, and ask {partner} for a rise.',
check: { attribute: 'charm', against: 'partner', opposedBy: 'wits', relationship: 'trust' },
success: {
result: '{partner} looks at you for a long moment and says the firm can manage something.',
effects: [
{ path: 'rises', op: 'add', value: 1 },
{ path: 'standing', op: 'add', value: 3 },
moment('Asked {partner} for a rise, and got it.'),
],
},
failure: {
result: '{partner} asks whether you have any idea what a clerk is worth. You find you do not.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: -5 },
{ path: 'standing', op: 'add', value: -2 },
],
},
},
{
id: 'hint',
label: 'Say nothing directly; mention the price of lodgings.',
check: { attribute: 'wits', difficulty: 5 },
success: {
result: '{partner} takes the point and, better, thinks it was a delicate one.',
effects: [{ path: 'relationships.partner.regard', op: 'add', value: 4 }],
},
failure: {
result: '{partner} agrees that lodgings are very dear and goes back to the post.',
effects: [],
},
},
{
id: 'not_yet',
label: 'Wait for a better moment.',
result: 'There will be one. There usually is.',
effects: [],
},
],
},
{
id: 'called_in',
stage: 'desk',
weight: 30,
cooldown: 8,
involves: ['clerk'],
description:
'{chief} has found a column that does not add up, and it is in your hand. You ' +
'are to explain it, standing, in front of the high desk.',
options: [
{
id: 'own_up',
label: 'Say plainly that it was your mistake.',
check: { attribute: 'resolve', difficulty: 4 },
success: {
result: 'It is noted, and then, which is rarer, it is let go.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: 4 },
{ path: 'attributes.resolve', op: 'add', value: 1 },
],
},
failure: {
result: 'You start well and finish apologising for the weather.',
effects: [{ path: 'standing', op: 'add', value: -2 }],
},
},
{
id: 'argue',
label: 'Argue that the figures were right.',
check: { attribute: 'wits', against: 'chief' },
success: {
result: 'They were. {chief} checks twice and hands the page back without a word.',
effects: [
{ path: 'standing', op: 'add', value: 3 },
{ path: 'relationships.chief.regard', op: 'add', value: -4 },
],
},
failure: {
result: 'They were not, and now everybody knows you thought they were.',
effects: [
{ path: 'standing', op: 'add', value: -4 },
{ path: 'relationships.partner.trust', op: 'add', value: -3 },
],
},
},
{
id: 'blame',
label: 'Say the figures came from {clerk}\'s desk.',
check: { attribute: 'wits', against: 'chief' },
success: {
result: '{chief} goes to find {clerk}. You do not watch.',
effects: [
{ path: 'standing', op: 'add', value: 1 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: -8 },
],
},
failure: {
result: '{chief} has the day book open at your page before you finish the sentence.',
effects: [
{ path: 'standing', op: 'add', value: -5 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: -10 },
{ path: 'relationships.partner.trust', op: 'add', value: -5 },
moment('Tried to put a mistake on {clerk}, and was found out.'),
],
},
},
],
},
{
id: 'the_door',
stage: 'desk',
weight: 30,
cooldown: 6,
description:
'A caller in a good coat wants {partner} and will not give his name. You ' +
'were told the partner was not to be disturbed before three.',
options: [
{
id: 'turn_away',
label: 'Tell him the partner is out.',
check: { attribute: 'nerve', difficulty: 5 },
success: {
result: 'He goes. {partner} hears of it later and approves of the lie.',
effects: [{ path: 'relationships.partner.trust', op: 'add', value: 3 }],
},
failure: {
result: 'He sits down to wait, and {partner} comes out at a quarter past and finds him there.',
effects: [
{ path: 'relationships.partner.regard', op: 'add', value: -3 },
{ path: 'standing', op: 'add', value: -1 },
],
},
},
{
id: 'size_him_up',
label: 'Size him up and decide for yourself.',
check: { attribute: 'wits', difficulty: 6 },
success: {
result: 'You read him right. He mattered, and he got in, and {partner} knows who let him.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: 5 },
{ path: 'standing', op: 'add', value: 2 },
moment('Judged a caller at the door, and judged him right.'),
],
},
failure: {
result: 'He was selling encyclopaedias, and {partner} bought one to get rid of him.',
effects: [{ path: 'relationships.partner.trust', op: 'add', value: -4 }],
},
},
{
id: 'pass_it_on',
label: 'Fetch {chief} and let it be somebody else\'s decision.',
result: '{chief} deals with it, and with you, briefly.',
effects: [{ path: 'standing', op: 'add', value: -1 }],
},
],
},
{
id: 'needled',
stage: 'desk',
weight: 40,
cooldown: 4,
involves: ['clerk'],
description:
'{clerk} reads your last letter aloud in a voice meant to be yours, and the ' +
'floor laughs, and keeps laughing longer than it deserves.',
options: [
{
id: 'let_it_go',
label: 'Let it pass.',
result: 'It passes. They will remember that you let it.',
effects: [
{ path: 'relationships.{clerk}.regard', op: 'add', value: 1 },
{ path: 'standing', op: 'add', value: -1 },
],
},
{
id: 'retort',
label: 'Answer {clerk} back, with the floor listening.',
check: { attribute: 'wits', against: '{clerk}', relationship: 'regard' },
success: {
result: 'It gets a bigger laugh than the letter did, and {clerk} laughs too.',
effects: [
{ path: 'standing', op: 'add', value: 2 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: 3 },
],
},
failure: {
result: 'It comes out too sharp, and the laugh turns on you.',
effects: [
{ path: 'standing', op: 'add', value: -2 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: -3 },
],
},
},
{
id: 'report',
label: 'Mention it to {chief}.',
result: '{chief} has a word. {clerk} knows where it came from.',
effects: [
{ path: 'relationships.chief.trust', op: 'add', value: 2 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: -6 },
{ path: 'standing', op: 'add', value: -1 },
],
},
],
},
{
id: 'red_ink',
stage: 'desk',
weight: 30,
cooldown: 6,
description:
'{chief} has gone over your morning\'s work in red ink, and left the page ' +
'open on the high desk where the whole floor can see it.',
options: [
{
id: 'swallow_it',
label: 'Take it, and do the page again.',
result: 'You do it again. {chief} initials it without looking up.',
effects: [
{ path: 'relationships.chief.regard', op: 'add', value: 2 },
{ path: 'standing', op: 'add', value: -1 },
],
},
{
id: 'answer_back',
label: 'Point out that the red ink is wrong.',
check: { attribute: 'wits', against: 'chief' },
success: {
result: 'It is, in two places. {chief} says nothing, which from {chief.first} is a concession.',
effects: [
{ path: 'standing', op: 'add', value: 2 },
{ path: 'relationships.chief.regard', op: 'add', value: 3 },
],
},
failure: {
result: 'The red ink is right. It usually is.',
effects: [
{ path: 'standing', op: 'add', value: -2 },
{ path: 'relationships.chief.regard', op: 'add', value: -5 },
],
},
},
{
id: 'over_the_head',
label: 'Take the page to {partner}.',
check: { attribute: 'charm', against: 'partner', opposedBy: 'resolve', relationship: 'trust' },
success: {
result: '{partner} reads it, and has a word with {chief}, and {chief} has one with you.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: 3 },
{ path: 'relationships.chief.regard', op: 'add', value: -8 },
{ path: 'standing', op: 'add', value: 2 },
moment('Went over {chief}\'s head, and it worked.'),
],
},
failure: {
result: '{partner} says the chief clerk runs the floor, and asks whether there was anything else.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: -4 },
{ path: 'relationships.chief.regard', op: 'add', value: -8 },
{ path: 'standing', op: 'add', value: -3 },
],
},
},
],
},
{
id: 'sure_thing',
stage: 'desk',
weight: 15,
cooldown: 20,
description:
'A pleasant man at the chop-house has a share in a tramway in Brazil going ' +
'begging. Twelve dollars today, he says, will be forty by Christmas.',
options: [
{
id: 'buy_in',
label: 'Put in twelve dollars.',
effects: [{ path: 'money', op: 'add', value: -12 }],
check: { attribute: 'wits', difficulty: 7 },
success: {
result: 'Against every expectation, including his, it pays.',
effects: [{ path: 'money', op: 'add', value: 30 }, moment('Bought a share in a tramway, and it paid.')],
},
failure: {
result: 'There is no tramway. There is, briefly, a man.',
effects: [moment('Bought a share in a tramway that did not exist.')],
},
},
{
id: 'see_through',
label: 'Ask him the one question he cannot answer.',
check: { attribute: 'wits', difficulty: 6 },
success: {
result: 'He finds he has an appointment. You find you are sharper than you were.',
effects: [{ path: 'attributes.wits', op: 'add', value: 1 }, moment('Saw through a tramway in Brazil.')],
},
failure: {
result: 'He answers it smoothly, and you are left not knowing.',
effects: [],
},
},
{
id: 'decline',
label: 'Keep your money in your pocket.',
result: 'He is sorry for you, he says, and goes to find someone less careful.',
effects: [],
},
],
},
{
id: 'the_round',
stage: 'desk',
weight: 30,
cooldown: 5,
involves: ['clerk'],
description:
'{clerk} and the others are going to the chop-house after work, and they ' +
'have kept you a place. The round there is not cheap.',
options: [
{
id: 'go',
label: 'Go, and stand your round.',
result: 'A good evening, and a dear one.',
effects: [
{ path: 'money', op: 'add', value: -5 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: 4 },
{ path: 'standing', op: 'add', value: 1 },
],
},
{
id: 'talk_your_way',
label: 'Go, and talk your way out of the round.',
check: { attribute: 'charm', against: '{clerk}', opposedBy: 'wits' },
success: {
result: 'Nobody notices you never reached for your purse, and {clerk} laughs at all your jokes.',
effects: [{ path: 'relationships.{clerk}.regard', op: 'add', value: 3 }],
},
failure: {
result: '{clerk} notices, and says so, and you pay for the round and the remark.',
effects: [
{ path: 'money', op: 'add', value: -5 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: -2 },
],
},
},
{
id: 'excuse',
label: 'Make an excuse and go home.',
result: 'They go without you. They will ask again, perhaps.',
effects: [{ path: 'relationships.{clerk}.regard', op: 'add', value: -2 }],
},
],
},
{
id: 'night_school',
stage: 'desk',
weight: 25,
cooldown: 5,
requires: [{ path: 'money', op: '>=', value: 3 }],
description:
'A card in the tram window: evening classes in bookkeeping, in the art of ' +
'address, and at the boxing club behind the Institute. Three dollars a course.',
options: [
{
id: 'bookkeeping',
label: 'Take the bookkeeping course.',
result: 'Double entry, twice a week. Your columns begin to add up before you do.',
effects: [{ path: 'money', op: 'add', value: -3 }, { path: 'attributes.wits', op: 'add', value: 1 }],
},
{
id: 'address',
label: 'Take the art of address.',
result: 'You learn when to say nothing, which is most of it.',
effects: [{ path: 'money', op: 'add', value: -3 }, { path: 'attributes.charm', op: 'add', value: 1 }],
},
{
id: 'boxing',
label: 'Join the boxing club.',
result: 'You are hit in the face by a draper. It does you a world of good.',
effects: [{ path: 'money', op: 'add', value: -3 }, { path: 'attributes.nerve', op: 'add', value: 1 }],
},
{
id: 'not_this_term',
label: 'Not this term.',
result: 'The card is still there next week.',
effects: [],
},
],
},
{
id: 'stand_in',
stage: 'desk',
weight: 15,
cooldown: 10,
requires: [{ path: 'standing', op: '>=', value: 45 }],
description:
'{partner} is laid up and sends you, of all people, to sit in on the meeting ' +
'with the shipping agents. You are to listen, and to say as little as possible.',
options: [
{
id: 'listen',
label: 'Listen, and take good notes.',
result: 'Your notes are better than the minutes. {partner} keeps them.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: 2 },
{ path: 'standing', op: 'add', value: 1 },
],
},
{
id: 'speak_up',
label: 'Say the thing you noticed in their figures.',
check: { attribute: 'wits', difficulty: 7 },
success: {
result: 'You were right, the agents know it, and so, by the afternoon, does {partner}.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: 6 },
{ path: 'standing', op: 'add', value: 5 },
moment('Spoke up at the agents\' meeting in {partner}\'s place, and was right.'),
],
},
failure: {
result: 'You were wrong in front of people who were paid to be right.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: -4 },
{ path: 'standing', op: 'add', value: -3 },
],
},
},
{
id: 'charm_them',
label: 'Charm the agents over lunch.',
check: { attribute: 'charm', difficulty: 6 },
success: {
result: 'They like you. They say so to {partner}, which is the point.',
effects: [
{ path: 'relationships.partner.regard', op: 'add', value: 5 },
{ path: 'standing', op: 'add', value: 3 },
],
},
failure: {
result: 'The lunch is long, and you pay for it.',
effects: [
{ path: 'money', op: 'add', value: -4 },
{ path: 'standing', op: 'add', value: -1 },
],
},
},
],
},
{
id: 'lost_bill',
stage: 'desk',
weight: 20,
cooldown: 12,
involves: ['clerk'],
description:
'{clerk} has lost a bill of lading, and {chief} is talking about a letter to ' +
'the partner. {clerk} has not looked up from the desk all morning.',
options: [
{
id: 'speak_for',
label: 'Put {clerk}\'s case to {partner} yourself.',
check: { attribute: 'charm', against: 'partner', opposedBy: 'resolve', relationship: 'trust' },
success: {
result: '{partner} hears you out and lets it go. {clerk} does not forget who spoke.',
effects: [
{ path: 'relationships.{clerk}.trust', op: 'add', value: 10 },
{ path: 'relationships.partner.regard', op: 'add', value: 2 },
moment('Spoke up for {clerk}, and it held.'),
],
},
failure: {
result: '{partner} asks whether you would like to share the letter.',
effects: [
{ path: 'relationships.partner.trust', op: 'add', value: -3 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: 4 },
],
},
},
{
id: 'help_look',
label: 'Stay late and help look for it.',
check: { attribute: 'resolve', difficulty: 5 },
success: {
result: 'It is in the wrong drawer, under the right one. You find it at nine.',
effects: [
{ path: 'relationships.{clerk}.trust', op: 'add', value: 8 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: 4 },
],
},
failure: {
result: 'It is not found, but {clerk} saw you look.',
effects: [{ path: 'relationships.{clerk}.regard', op: 'add', value: 2 }],
},
},
{
id: 'keep_out',
label: 'Keep out of it.',
result: 'The letter goes. So, very nearly, does {clerk}.',
effects: [{ path: 'relationships.{clerk}.trust', op: 'add', value: -3 }],
},
],
},
{
id: 'the_butt',
stage: 'desk',
weight: 20,
cooldown: 8,
involves: ['clerk'],
description:
'Somebody has pinned a card to your back, and you have walked the length of ' +
'the floor to {chief}\'s desk wearing it. {clerk} cannot keep a straight face.',
options: [
{
id: 'laugh_along',
label: 'Laugh louder than anybody.',
check: { attribute: 'charm', difficulty: 4 },
success: {
result: 'It becomes your joke instead of theirs.',
effects: [
{ path: 'standing', op: 'add', value: 1 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: 3 },
],
},
failure: {
result: 'The laugh comes out wrong, and everyone hears that it did.',
effects: [{ path: 'standing', op: 'add', value: -2 }],
},
},
{
id: 'dignity',
label: 'Take it off and say nothing at all.',
check: { attribute: 'resolve', difficulty: 4 },
success: {
result: 'It is not funny for long, when nobody is laughing at the front.',
effects: [{ path: 'standing', op: 'add', value: 1 }],
},
failure: {
result: 'Your ears give you away.',
effects: [
{ path: 'standing', op: 'add', value: -1 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: -2 },
],
},
},
{
id: 'get_even',
label: 'Resolve to do something about {clerk} tomorrow.',
result: 'You do. It is not as funny, and {clerk} knows exactly who it was.',
effects: [
{ path: 'relationships.{clerk}.regard', op: 'add', value: -2 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: -3 },
],
},
],
},
{
id: 'saturday',
stage: 'desk',
weight: 60,
cooldown: 5,
requires: [{ path: 'calendar.name', op: '==', value: 'Saturday' }],
description:
'Saturday, and the office shuts at one. The pay envelope is in your inside ' +
'pocket, and the whole of the afternoon is in front of you.',
options: [
{
id: 'new_collar',
label: 'Buy the collar the senior clerks wear.',
result: 'It pinches, and it is noticed.',
effects: [
{ path: 'money', op: 'add', value: -5 },
{ path: 'standing', op: 'add', value: 2 },
],
},
{
id: 'good_dinner',
label: 'Dine somewhere with a tablecloth.',
result: 'A dinner you will describe, later, to people who were not there.',
effects: [{ path: 'money', op: 'add', value: -4 }],
},
{
id: 'pay_back',
label: 'Pay back what you owe the floor.',
requires: [{ path: 'owed', op: '>', value: 0 }],
whenLocked: 'hide',
result: 'Squared, or nearer it. It is a better feeling than the dinner would have been.',
// The money is read from the balance before either moves, so a small
// debt costs what it is and no more (DECISIONS #38).
effects: [
{ path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 10 } },
{ path: 'owed', op: 'add', value: { from: 'owed', times: -1, cap: 10 } },
],
},
{
id: 'straight_home',
label: 'Straight home, envelope sealed.',
result: 'The envelope is lighter than it was in your imagination, but it is all there.',
effects: [],
},
],
},
{
id: 'family_errand',
stage: 'desk',
weight: 20,
cooldown: 8,
involves: ['clerk'],
description:
'A parcel for {partner}\'s sister has to cross the city before four, in the ' +
'rain, and {partner} has asked for you by name. It is not in the job.',
options: [
{
id: 'take_it',
label: 'Take it yourself, and pay your own tram.',
result: 'The sister gives you tea, and a message for {partner} that is more about you than the parcel.',
effects: [
{ path: 'money', op: 'add', value: -1 },
{ path: 'relationships.partner.regard', op: 'add', value: 4 },
{ path: 'standing', op: 'add', value: -1 },
],
},
{
id: 'make_an_impression',
label: 'Take it, and make yourself agreeable when you get there.',
check: { attribute: 'charm', difficulty: 5 },
success: {
result: 'The sister writes to {partner} that evening. You are mentioned warmly.',
effects: [
{ path: 'relationships.partner.regard', op: 'add', value: 6 },
{ path: 'relationships.partner.trust', op: 'add', value: 2 },
moment('Carried a parcel for {partner}\'s sister, and made a friend of her.'),
],
},
failure: {
result: 'You drip on the carpet and say something about the weather twice.',
effects: [{ path: 'relationships.partner.regard', op: 'add', value: 1 }],
},
},
{
id: 'hand_it_on',
label: 'Hand it on to {clerk}, who is going that way.',
result: '{clerk} goes, and gets the tea, and does not say whose errand it was.',
effects: [
{ path: 'relationships.{clerk}.regard', op: 'add', value: -3 },
{ path: 'relationships.partner.regard', op: 'add', value: -2 },
],
},
],
},
{
id: 'window_desk',
stage: 'desk',
weight: 20,
cooldown: 10,
involves: ['clerk'],
description:
'The desk by the window has come free. {chief} says it will go to whoever ' +
'deserves it, and {clerk} has been in before the clock all week.',
options: [
{
id: 'outwork',
label: 'Come in earlier than {clerk} does.',
check: { attribute: 'resolve', against: '{clerk}' },
success: {
result: 'You are there before the cleaners. The desk is yours, and so is the draught.',
effects: [
{ path: 'standing', op: 'add', value: 4 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: -3 },
moment('Won the desk by the window from {clerk}.'),
],
},
failure: {
result: '{clerk} was there before you, every morning, and has the desk.',
effects: [{ path: 'standing', op: 'add', value: -1 }],
},
},
{
id: 'word_with_chief',
label: 'Have a quiet word with {chief}.',
check: { attribute: 'charm', against: 'chief', opposedBy: 'wits', relationship: 'regard' },
success: {
result: '{chief} decides the matter in your favour, and makes sure {clerk} knows why.',
effects: [
{ path: 'standing', op: 'add', value: 3 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: -6 },
],
},
failure: {
result: '{chief} writes your name down, next to the word "canvassing".',
effects: [
{ path: 'relationships.chief.trust', op: 'add', value: -4 },
{ path: 'standing', op: 'add', value: -2 },
],
},
},
{
id: 'let_them_have_it',
label: 'Let {clerk} have it.',
result: '{clerk} takes the desk and, to your surprise, remembers who did not fight for it.',
effects: [
{ path: 'relationships.{clerk}.regard', op: 'add', value: 5 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: 3 },
],
},
],
},
{
id: 'hard_times',
stage: 'desk',
weight: 400,
cooldown: 2,
requires: [{ path: 'money', op: '<', value: 0 }],
involves: ['clerk'],
description:
'The landlady has knocked twice about the week, and the purse under your ' +
'pillow is empty. It is a long walk to Saturday.',
options: [
{
id: 'copying',
label: 'Take in copying work to do at night.',
result: 'Law papers, by lamp, at a halfpenny a folio. It keeps the landlady quiet.',
effects: [
{ path: 'money', op: 'add', value: 8 },
{ path: 'attributes.resolve', op: 'add', value: 1 },
],
},
{
id: 'borrow',
label: 'Ask {clerk} for the loan of ten dollars.',
check: { attribute: 'charm', against: '{clerk}', opposedBy: 'resolve', relationship: 'trust' },
success: {
result: '{clerk} lends it, and tells you not to mention it, and you will not forget.',
effects: [
{ path: 'money', op: 'add', value: 10 },
{ path: 'owed', op: 'add', value: 10 },
{ path: 'relationships.{clerk}.trust', op: 'add', value: -2 },
],
},
failure: {
result: '{clerk} is short too, but finds you three, and makes a face about it.',
effects: [
{ path: 'money', op: 'add', value: 3 },
{ path: 'owed', op: 'add', value: 3 },
{ path: 'relationships.{clerk}.regard', op: 'add', value: -3 },
],
},
},
{
id: 'pawn_the_watch',
label: 'Pawn your father\'s watch.',
requires: [{ path: 'flags.pawned_watch', op: '==', value: false }],
whenLocked: 'hide',
result: 'Twelve dollars and a ticket. You will get it back. Everybody says that.',
effects: [
{ path: 'money', op: 'add', value: 12 },
{ path: 'flags.pawned_watch', op: 'set', value: true },
moment('Pawned your father\'s watch to pay the landlady.'),
],
},
],
},
];
export default events;
-136
View File
@@ -1,136 +0,0 @@
// The Counting House — a prototype pack for checks and a drawn cast.
//
// Harrow & Pike, a trading firm's counting house, somewhere about 1910. You
// are the newest clerk. The situations are the ones public-domain office
// fiction kept returning to (tools/story-to-pack, catalogue v3); the people are
// drawn fresh each run from several candidates per role, two to four clerks
// among them; and most choices are a check, shown to the player only as odds
// in words. See DECISIONS #41.
//
// One stage, deliberately: this pack exists to find out whether chance and a
// varying cast make the same situations worth replaying, before a whole career
// is built on them.
import { characters } from './characters.js';
import { events } from './events.js';
export const countingHouse = {
id: 'countinghouse',
name: 'The Counting House',
blurb:
'Harrow & Pike, about 1910: a stool, a pen, a chief clerk with red ink, and a ' +
'different office every time you start.',
version: '0.1.0',
turnUnit: 'day',
startingStage: 'desk',
peopleHeading: 'Harrow & Pike',
stages: {
desk: { label: 'The Desk', title: 'Junior Clerk' },
},
// A six-day week: the office shuts at one on Saturday, and that is pay day.
calendar: {
cycleLength: 6,
cycleName: 'Week',
unitNames: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
},
// The four attributes, the same for the player and for everyone in the cast.
// A pack may call them what it likes; these are the prototype's names.
attributes: {
nerve: { label: 'Nerve' },
wits: { label: 'Wits' },
charm: { label: 'Charm' },
resolve: { label: 'Resolve' },
},
// How attributes turn into chances. The player sees only the words.
// chance = 50 + 8 x (yours - theirs) + 0.4 x (their regard - 50), from 5 to 95
// A new clerk (3) against a sharp partner (8) is 10%, a long shot; against an
// ordinary task (4) it is 42%. Each point of training is worth eight.
checks: {
base: 50,
perPoint: 8,
unopposed: 4,
relationshipPerPoint: 0.4,
relationshipCentre: 50,
floor: 5,
ceiling: 95,
odds: [
{ upTo: 20, words: 'a long shot' },
{ upTo: 40, words: 'unlikely' },
{ upTo: 60, words: 'an even chance' },
{ upTo: 80, words: 'likely' },
{ upTo: 95, words: 'a safe bet' },
],
said: { success: 'and it came off.', failure: 'and it did not come off.' },
},
state: {
'money': { initial: 20, min: -200, max: 100000 },
'standing': { initial: 40, min: 0, max: 100 },
'attributes.nerve': { initial: 3, min: 0, max: 10 },
'attributes.wits': { initial: 3, min: 0, max: 10 },
'attributes.charm': { initial: 3, min: 0, max: 10 },
'attributes.resolve': { initial: 3, min: 0, max: 10 },
// Borrowed from the other clerks when times are hard, repaid on a Saturday.
'owed': { initial: 0, min: 0, max: 100000 },
// Rises won from the partner: each is a permanent addition to the wage.
'rises': { initial: 0, min: 0, max: 3 },
'flags.pawned_watch': { initial: false },
'notable': { initial: [] },
},
// Two axes, not one: whether they believe you, and what they think of you.
relationshipStats: {
trust: { label: 'Trusts you', initial: 50, min: 0, max: 100 },
regard: { label: 'Thinks well of you', initial: 50, min: 0, max: 100 },
},
characters,
events,
// Per six-day week: -12 board and lodging +16 wages = +4.
// Each rise adds four; a rise is earned, so it is outside the baseline.
upkeep: [
{ label: 'Board and lodging', effects: [{ path: 'money', op: 'add', value: -2 }] },
{
label: 'Wages from Harrow & Pike',
every: 6,
offset: 5,
effects: [{ path: 'money', op: 'add', value: 16 }],
},
...[1, 2, 3].map((n) => ({
label: n === 1 ? 'Your rise' : `Your rise (${n})`,
every: 6,
offset: 5,
requires: [{ path: 'rises', op: '>=', value: n }],
effects: [{ path: 'money', op: 'add', value: 4 }],
})),
],
tuning: {
baselinePerCycle: { desk: [0, 10] },
baselineExcludes: ['rises'],
},
display: {
'money': { label: 'Money', format: 'money', order: 1 },
'owed': { label: 'Owed to the floor', format: 'money', order: 2, hideWhenZero: true },
'standing': { label: 'Standing', format: 'meter', order: 2 },
'attributes.nerve': { label: 'Nerve', format: 'meter', order: 3 },
'attributes.wits': { label: 'Wits', format: 'meter', order: 4 },
'attributes.charm': { label: 'Charm', format: 'meter', order: 5 },
'attributes.resolve': { label: 'Resolve', format: 'meter', order: 6 },
'rises': { label: 'Rises', order: 7, hideWhenZero: true },
'notable': { hidden: true, inLedger: false },
'flags.pawned_watch': {
label: 'Your father\'s watch', hidden: true,
whenTrue: 'Your father\'s watch is at the pawnbroker\'s.',
whenFalse: 'You have your father\'s watch back.',
},
},
};
export default countingHouse;
+15 -300
View File
@@ -160,7 +160,7 @@ export const deputy = [
options: [ options: [
{ {
id: 'wade_in', id: 'wade_in',
label: 'Go in and take hold of the nearest man who is fighting.', label: 'Go in and take hold of the nearest one.',
result: result:
'It stops. Your sleeve does not survive it and neither does a ' + 'It stops. Your sleeve does not survive it and neither does a ' +
'table, and the whole street knows by morning which it was.', 'table, and the whole street knows by morning which it was.',
@@ -580,281 +580,15 @@ export const deputy = [
], ],
}, },
// Five more days in the office, added after the first real play of this
// stage: 69 turns saw 13 distinct situations, and `office_day` and
// `the_cells` were a sixth of the stage each. See DECISIONS #40.
{
id: 'stage_came_in_short',
stage: 'deputy',
weight: 50,
cooldown: 7,
description:
'The stage came in with the strongbox eleven pounds lighter than the ' +
'manifest, and the driver swears on his mother that he never once ' +
'stopped between the Wells and here.',
options: [
{
id: 'wire_the_county',
label: 'Wire the county and let them send a man.',
result:
'The county sends a man in nine days, by which time the driver has ' +
'told the story often enough to believe every word of it.',
effects: [
{ path: 'skills.trade', op: 'add', value: 2 },
{ path: 'relationships.ward.trusts_you', op: 'add', value: 2 },
{ path: 'standing', op: 'add', value: -1 },
],
},
{
id: 'ride_the_route',
label: 'Ride the route back yourself, looking at the ground.',
result:
'Wheel ruts off the road at the second creek, where the driver says ' +
'he never stopped. You do not find the eleven pounds. You find where ' +
'they got off.',
effects: [
{ path: 'money', op: 'add', value: -3 },
{ path: 'skills.nerve', op: 'add', value: 3 },
{ path: 'standing', op: 'add', value: 3 },
],
},
{
id: 'lean_on_the_driver',
label: 'Lean on the driver until his story changes.',
requires: [{ path: 'skills.nerve', op: '>=', value: 30 }],
result:
'It changes on the fourth telling, at the second creek, and then ' +
'again at the part where somebody was expecting him.',
effects: [
{ path: 'skills.nerve', op: 'add', value: 3 },
{ path: 'standing', op: 'add', value: 4 },
{ path: 'relationships.ward.wary_of_you', op: 'add', value: 5 },
{ path: 'notable', op: 'push', value: 'Leaned on a stage driver until the story changed.' },
],
},
],
},
{
id: 'the_county_fees',
stage: 'deputy',
weight: 45,
cooldown: 8,
description:
'The county wants its licence fees collected from every business on the ' +
'street, and the list it sent includes the woman who sells pies out of ' +
'her kitchen window on Sundays.',
options: [
{
id: 'collect_all',
label: 'Collect from everybody on the list.',
result:
'All of it, pies included. The county is pleased. The street is ' +
'polite to you in a way it was not last week.',
effects: [
{ path: 'standing', op: 'add', value: -4 },
{ path: 'skills.trade', op: 'add', value: 1 },
{ path: 'relationships.ward.trusts_you', op: 'add', value: 4 },
],
},
{
id: 'pay_hers',
label: 'Collect from the rest, and pay hers out of your own pocket.',
result:
'A dollar fifty. Nobody is told, and by Sunday everybody knows, which ' +
'is how the street keeps its accounts.',
effects: [
{ path: 'money', op: 'add', value: -5 },
{ path: 'standing', op: 'add', value: 5 },
{ path: 'notable', op: 'push', value: 'Paid the pie licence out of your own pocket.' },
],
},
{
id: 'strike_her_off',
label: 'Strike her off the list and let the county notice if it can.',
requires: [{ path: 'skills.trade', op: '>=', value: 28 }],
result:
'You find the statute that exempts a kitchen window, and cite it in ' +
'the return. The county never writes back, which is the county ' +
'agreeing.',
effects: [
{ path: 'skills.trade', op: 'add', value: 3 },
{ path: 'standing', op: 'add', value: 3 },
{ path: 'relationships.ward.wary_of_you', op: 'add', value: 4 },
],
},
],
},
{
id: 'the_bounty_man',
stage: 'deputy',
weight: 40,
cooldown: 9,
description:
'A man with a paper from Bitter Wells and a rifle he keeps too clean ' +
'wants to know whether a teamster called Pike is in Cold Fork. There is ' +
'a teamster called Pike in Cold Fork, and he has been decent to you.',
options: [
{
id: 'tell_him',
label: 'Tell him where Pike is.',
result:
'He thanks you and leaves money on the desk, which you did not ask ' +
'for and do not give back. The teamsters hear about the money first.',
effects: [
{ path: 'money', op: 'add', value: 6 },
{ path: 'standing', op: 'add', value: -3 },
{ path: 'relationships.ward.trusts_you', op: 'add', value: 3 },
{ path: 'relationships.dutch.wary_of_you', op: 'add', value: 6 },
],
},
{
id: 'send_him_on',
label: 'Tell him Pike went west a month ago.',
result:
'He looks at you for long enough that you understand he knows. Then ' +
'he rides west, because the paper does not pay him to argue.',
effects: [
{ path: 'skills.nerve', op: 'add', value: 2 },
{ path: 'relationships.dutch.trusts_you', op: 'add', value: 5 },
{ path: 'relationships.ward.wary_of_you', op: 'add', value: 5 },
],
},
{
id: 'check_the_paper',
label: 'Read his paper properly before you say anything at all.',
requires: [{ path: 'skills.trade', op: '>=', value: 30 }],
result:
'It names a Pike, a year and a county, and two of the three are ' +
'wrong. You tell him so, with the statute, and he goes home.',
effects: [
{ path: 'skills.trade', op: 'add', value: 3 },
{ path: 'standing', op: 'add', value: 4 },
{ path: 'relationships.ward.trusts_you', op: 'add', value: 4 },
{ path: 'notable', op: 'push', value: 'Read a bounty man’s paper closely enough to send him home.' },
],
},
],
},
{
id: 'fire_at_the_store',
stage: 'deputy',
weight: 35,
cooldown: 12,
description:
'A lamp goes over in the back room of the store an hour after midnight, ' +
'and by the time you are in your boots the whole street is standing in ' +
'it in its nightclothes.',
options: [
{
id: 'bucket_line',
label: 'Get a bucket line going from the trough.',
result:
'It is out by two. The back room is gone and the front is black to ' +
'the ceiling, and the storekeeper shakes every hand on the line.',
effects: [
{ path: 'money', op: 'add', value: -2 },
{ path: 'skills.nerve', op: 'add', value: 2 },
{ path: 'standing', op: 'add', value: 5 },
],
},
{
id: 'go_in_for_the_book',
label: 'Go in for the book on the counter.',
requires: [{ path: 'skills.nerve', op: '>=', value: 34 }],
result:
'You come out with it under your coat and no eyebrows. Half the town ' +
'is in that book, and every one of them knows who carried it out.',
effects: [
{ path: 'skills.nerve', op: 'add', value: 4 },
{ path: 'standing', op: 'add', value: 6 },
{ path: 'notable', op: 'push', value: 'Went into the burning store for the book.' },
],
},
{
id: 'hold_the_crowd',
label: 'Keep the crowd back and let it burn itself out.',
result:
'Nobody else is hurt, which Ward says is the only part of a fire that ' +
'is the office’s business. The storekeeper does not agree, loudly.',
effects: [
{ path: 'skills.trade', op: 'add', value: 2 },
{ path: 'standing', op: 'add', value: -2 },
{ path: 'relationships.ward.trusts_you', op: 'add', value: 3 },
],
},
],
},
{
id: 'dutch_wants_the_ridge',
stage: 'deputy',
weight: 40,
cooldown: 10,
description:
'Dutch comes to the office sober and early, which has not happened ' +
'before, and asks whether a deputy has any say in who drives the Ridge ' +
'road now that Vane is short a man.',
options: [
{
id: 'put_him_forward',
label: 'Say you will put his name in with Vane.',
result:
'He nods once and goes, and Vane hears about it from Dutch before he ' +
'hears about it from you, which is not how you meant it to go.',
effects: [
{ path: 'relationships.dutch.trusts_you', op: 'add', value: 8 },
{ path: 'relationships.dutch.owes_you', op: 'add', value: 10 },
{ path: 'relationships.hollis.wary_of_you', op: 'add', value: 5 },
],
},
{
id: 'office_stays_out',
label: 'Say the office does not hire teamsters.',
result:
'He says that is true, and it is, and he takes his hat off the desk ' +
'and goes back down the street to wait for somebody else to decide.',
effects: [
{ path: 'standing', op: 'add', value: 2 },
{ path: 'relationships.dutch.trusts_you', op: 'add', value: -5 },
{ path: 'relationships.ward.trusts_you', op: 'add', value: 2 },
],
},
{
id: 'ask_vane_for_him',
label: 'Walk across the street and ask Vane yourself.',
requires: [{ path: 'relationships.hollis.trusts_you', op: '>=', value: 40 }],
result:
'Vane hears you out in the doorway and says he will think about it, ' +
'and Dutch is on the Ridge road on Monday.',
effects: [
{ path: 'standing', op: 'add', value: 3 },
{ path: 'relationships.dutch.trusts_you', op: 'add', value: 10 },
{ path: 'relationships.hollis.trusts_you', op: 'add', value: -2 },
{ path: 'notable', op: 'push', value: 'Asked Vane to put Dutch on the Ridge road.' },
],
},
],
},
{ {
// Corporate Ladder's car lot, in Cold Fork's terms and at its scale. Same // Corporate Ladder's car lot, in Cold Fork's terms and at its scale. Same
// structure deliberately: an offer, never an obligation, with the weekly // structure deliberately: an offer, never an obligation, with the weekly
// charge for falling short of it sitting in upkeep instead. // charge for falling short of it sitting in upkeep instead.
//
// The horse and the room used to be one event, whose description was
// always about the horse. A real session was offered the room under that
// text, and shown "Buy the bay" locked after buying it — a door that could
// never open. Now each is its own event, gated on the tier it offers, so
// neither can appear once it is bought. See DECISIONS #40.
id: 'the_sale_barn', id: 'the_sale_barn',
stage: 'deputy', stage: 'deputy',
weight: 45, weight: 45,
cooldown: 6, cooldown: 6,
requires: [{ path: 'lifestyle', op: '==', value: 0 }], requires: [{ path: 'lifestyle', op: '<', value: 2 }],
description: description:
'The sale barn has a bay with four good years in him and a price on ' + 'The sale barn has a bay with four good years in him and a price on ' +
'him. Ward says nothing about it, except that the county pays mileage ' + 'him. Ward says nothing about it, except that the county pays mileage ' +
@@ -863,7 +597,10 @@ export const deputy = [
{ {
id: 'buy_the_bay', id: 'buy_the_bay',
label: 'Buy the bay.', label: 'Buy the bay.',
requires: [{ path: 'money', op: '>=', value: 70 }], requires: [
{ path: 'lifestyle', op: '==', value: 0 },
{ path: 'money', op: '>=', value: 70 },
],
result: result:
'He is a better animal than the livery has ever put under you, and ' + 'He is a better animal than the livery has ever put under you, and ' +
'the whole street watches you ride him down it once.', 'the whole street watches you ride him down it once.',
@@ -874,32 +611,13 @@ export const deputy = [
{ path: 'notable', op: 'push', value: 'Bought the bay out of the sale barn.' }, { path: 'notable', op: 'push', value: 'Bought the bay out of the sale barn.' },
], ],
}, },
{
id: 'ride_the_livery',
label: 'Ride what the livery gives you.',
result:
'A different animal every week, and you learn something about each ' +
'one of them in the first mile.',
effects: [{ path: 'skills.trade', op: 'add', value: 1 }],
},
],
},
{
id: 'the_ellis_room',
stage: 'deputy',
weight: 45,
cooldown: 6,
requires: [{ path: 'lifestyle', op: '==', value: 1 }],
description:
'The room over the Ellis place is empty again: a stove, a door that ' +
'locks, and a window over the street. The Cordwood charges a deputy the ' +
'same as a teamster, and says so to both of them.',
options: [
{ {
id: 'take_the_room', id: 'take_the_room',
label: 'Take the room over the Ellis place, with the stove.', label: 'Take the room over the Ellis place, with the stove.',
requires: [{ path: 'money', op: '>=', value: 150 }], requires: [
{ path: 'lifestyle', op: '==', value: 1 },
{ path: 'money', op: '>=', value: 150 },
],
result: result:
'A door that locks, a stove, and a window over the street. The ' + 'A door that locks, a stove, and a window over the street. The ' +
'Cordwood does not hold it against you; the Cordwood holds nothing ' + 'Cordwood does not hold it against you; the Cordwood holds nothing ' +
@@ -912,15 +630,12 @@ export const deputy = [
], ],
}, },
{ {
id: 'stay_at_the_cordwood', id: 'ride_the_livery',
label: 'Stay at the Cordwood.', label: 'Ride what the livery gives you.',
result: result:
'The walls are thin and the talk comes through them, and some of what ' + 'A different animal every week, and you learn something about each ' +
'comes through is worth a deputy hearing.', 'one of them in the first mile.',
effects: [ effects: [{ path: 'skills.trade', op: 'add', value: 1 }],
{ path: 'skills.trade', op: 'add', value: 1 },
{ path: 'relationships.dutch.trusts_you', op: 'add', value: 2 },
],
}, },
], ],
}, },
+6 -9
View File
@@ -99,16 +99,14 @@ export const hardTimes = [
'book and yours is one of them. He mentions this to you in the street, ' + 'book and yours is one of them. He mentions this to you in the street, ' +
'in daylight, in front of two other people.', 'in daylight, in front of two other people.',
options: [ options: [
// Both payments take what is owed up to their size, never more — money
// first, because both effects read `owed` before it moves. See #38.
{ {
id: 'pay_chunk', id: 'pay_chunk',
label: 'Pay a real piece of it down, up to forty-five dollars.', label: 'Pay a real piece of it down.',
requires: [{ path: 'money', op: '>=', value: 45 }], requires: [{ path: 'money', op: '>=', value: 45 }],
result: 'He rules a line through most of it and is disappointed about it.', result: 'He rules a line through most of it and is disappointed about it.',
effects: [ effects: [
{ path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 45 } }, { path: 'money', op: 'add', value: -45 },
{ path: 'owed', op: 'add', value: { from: 'owed', times: -1, cap: 45 } }, { path: 'owed', op: 'add', value: -45 },
{ path: 'standing', op: 'add', value: 2 }, { path: 'standing', op: 'add', value: 2 },
], ],
}, },
@@ -116,11 +114,10 @@ export const hardTimes = [
id: 'pay_minimum', id: 'pay_minimum',
label: 'Pay what you have on you.', label: 'Pay what you have on you.',
requires: [{ path: 'money', op: '>=', value: 12 }], requires: [{ path: 'money', op: '>=', value: 12 }],
result: 'Up to ten dollars against the line, and two dollars of coffee while you are in there.', result: 'Twelve dollars, ten of which is the line and two of which is coffee.',
effects: [ effects: [
{ path: 'money', op: 'add', value: -2 }, { path: 'money', op: 'add', value: -12 },
{ path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 10 } }, { path: 'owed', op: 'add', value: -10 },
{ path: 'owed', op: 'add', value: { from: 'owed', times: -1, cap: 10 } },
], ],
}, },
{ {
-52
View File
@@ -10,58 +10,6 @@
// than a mistake. // than a mistake.
export const promotion = [ export const promotion = [
{
// The first real player asked, thirty turns in, how they would know they
// were getting close. Nothing said. This does, in the town's own terms: it
// names what the offer is waiting for without printing a threshold. It
// comes once, somewhat before the offer can, and only to a hand already
// heading that way. See DECISIONS #40.
id: 'ward_takes_notice',
stage: 'hand',
weight: 300,
once: true,
requires: [
{ path: 'turn', op: '>=', value: 24 },
{ path: 'standing', op: '>=', value: 55 },
],
description:
'Ward stops at the yard gate on his way past and says, to nobody in ' +
'particular, that the county will want a second man by winter. The man ' +
'will need a name the street says for the right reasons, and he will ' +
'need to be one Ward can take at his word — or one who has already ' +
'walked into trouble on the Ridge road and walked back out of it.',
options: [
{
id: 'ask_what_it_takes',
label: 'Ask him how long a man would have to wait.',
result:
'He says not before the fortieth day of anybody’s employment, and ' +
'that he is not talking about anybody in particular, and goes on up ' +
'the street.',
effects: [
{ path: 'relationships.ward.trusts_you', op: 'add', value: 3 },
{ path: 'notable', op: 'push', value: 'Heard from Ward that the county would want a second man.' },
],
},
{
id: 'happy_in_the_yard',
label: 'Say you are happy in the yard.',
result: 'Vane, in the office doorway, hears you say it.',
effects: [
{ path: 'relationships.hollis.trusts_you', op: 'add', value: 3 },
],
},
{
id: 'remember_it',
label: 'Say nothing, and remember it.',
result: 'He did not expect an answer, and seems to like not getting one.',
effects: [
{ path: 'skills.nerve', op: 'add', value: 1 },
],
},
],
},
{ {
id: 'deputy_offer', id: 'deputy_offer',
stage: 'hand', stage: 'hand',
+4 -8
View File
@@ -243,7 +243,7 @@ export const routine = [
}, },
{ {
id: 'stay_out', id: 'stay_out',
label: 'Stay out of it, and take neither side about the axle.', label: 'Have no view on the axle.',
result: 'Both of them decide, separately, that you are no use to them.', result: 'Both of them decide, separately, that you are no use to them.',
effects: [ effects: [
{ path: 'relationships.dutch.trusts_you', op: 'add', value: -4 }, { path: 'relationships.dutch.trusts_you', op: 'add', value: -4 },
@@ -298,7 +298,7 @@ export const routine = [
}, },
{ {
id: 'pay_down', id: 'pay_down',
label: 'Pay the book down, up to twenty dollars.', label: 'Put money against the book.',
// Hidden rather than greyed out when there is nothing owed. A locked // Hidden rather than greyed out when there is nothing owed. A locked
// door is worth showing when it is a door the player could open later // door is worth showing when it is a door the player could open later
// by playing differently; "pay down what you owe" shown to a man who // by playing differently; "pay down what you owe" shown to a man who
@@ -310,13 +310,9 @@ export const routine = [
{ path: 'money', op: '>=', value: 20 }, { path: 'money', op: '>=', value: 20 },
], ],
result: 'He crosses a line through it and turns the page back over.', result: 'He crosses a line through it and turns the page back over.',
// What is owed, up to twenty, and never more. A flat -20 against a
// four-dollar balance took sixteen dollars for nothing in a real
// session. Money comes first: both effects read `owed` before it moves.
// See DECISIONS #38.
effects: [ effects: [
{ path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 20 } }, { path: 'money', op: 'add', value: -20 },
{ path: 'owed', op: 'add', value: { from: 'owed', times: -1, cap: 20 } }, { path: 'owed', op: 'add', value: -20 },
{ path: 'standing', op: 'add', value: 2 }, { path: 'standing', op: 'add', value: 2 },
], ],
}, },
+3 -6
View File
@@ -20,7 +20,7 @@ export const frontier = {
blurb: blurb:
'Cold Fork, at the end of a stage line: a freight yard, a road that is ' + '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.', 'not safe, and a marshal who has been six years retiring.',
version: '0.1.1', version: '0.1.0',
turnUnit: 'day', turnUnit: 'day',
startingStage: 'hand', startingStage: 'hand',
peopleHeading: 'Cold Fork', peopleHeading: 'Cold Fork',
@@ -100,18 +100,15 @@ export const frontier = {
offset: 3, offset: 3,
effects: [{ path: 'money', op: 'add', value: -8 }], 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', label: 'Settlement',
every: 4, every: 4,
offset: 4, offset: 4,
requires: [{ path: 'stage', op: '==', value: 'hand' }], requires: [{ path: 'stage', op: '==', value: 'hand' }],
effects: [{ path: 'money', op: 'add', value: 24 }], effects: [{ path: 'money', op: 'add', value: 24 }],
}, },
{ {
label: 'Wages from the county', label: 'Deputy pay',
every: 4, every: 4,
offset: 4, offset: 4,
// The town pays a real wage, and it is still not much of a living. What // The town pays a real wage, and it is still not much of a living. What
+1 -2
View File
@@ -10,9 +10,8 @@
import { corporateLadder } from './corporateladder/index.js'; import { corporateLadder } from './corporateladder/index.js';
import { frontier } from './frontier/index.js'; import { frontier } from './frontier/index.js';
import { countingHouse } from './countinghouse/index.js';
export const packs = [corporateLadder, frontier, countingHouse]; export const packs = [corporateLadder, frontier];
/** A pack by id, or undefined. Used to resume into whatever was last played. */ /** A pack by id, or undefined. Used to resume into whatever was last played. */
export function packById(id) { export function packById(id) {
-198
View File
@@ -656,201 +656,3 @@ promoted is not independent of how they spend, so the comparison mostly measured
the mix. It inverted the moment there was something to spend money on. It is now the mix. It inverted the moment there was something to spend money on. It is now
derived from upkeep arithmetic, and the lifestyle's effect is tested with two derived from upkeep arithmetic, and the lifestyle's effect is tested with two
players who differ in exactly one habit. players who differ in exactly one habit.
## 38. A repayment pays what is owed, never more
Recorded 2026-09-15, for 0.3.4. Designer's call: a small engine addition rather
than a content workaround.
The first human session of the Frontier on a current build — 112 turns — owed the
store four dollars on turn 27 and chose "Put money against the book". It took
twenty dollars and cleared four: the option was a flat `money -20, owed -20`, and
`owed` clamped at zero while `money` did not. The ledger showed "−$4 at its
limit", which the player read, reasonably, as a debt still standing.
The same shape was in five options across both packs — the Frontier's store and
both of its hard-times repayments, and both of Corporate Ladder's loan
repayments — and none had been caught, because every archetype that pays debt
pays large debts.
**Content could not fix it.** Effects add or set a number the pack wrote, and a
balance built from +14, +32, +4, −45 and −10 can be anything, so no fixed payment
can equal it. The alternatives were to gate each instalment on the balance
covering it (leaving small balances to cost more than they are) or to retune
every credit amount to a common step (changing tuned numbers to suit a missing
primitive).
**So an `add` effect's value may now be read from state:**
`{ path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 20 } }` is
"take away whatever is owed, but no more than 20". The amount is resolved at the
moment the effect applies, so a repayment lists the money effect before the
balance effect — both read the balance before it moves. The change record keeps
the number actually applied, with the reference in `source`.
This is not the engine learning about debt. It reads a number from a path without
knowing what the path means, exactly as a condition does, and #1 and #2 still
hold: it is data, validated at load. The validator requires the referenced path to
be declared and `times` and `cap` to be numbers. The test archetypes score such an
effect at its cap, the most a player reading the label could expect.
A conformance test now holds every pack to it: an option that spends money and
reduces a balance — any money-formatted stat other than money — must read the
amount from the balance, or be gated on the balance covering the whole instalment.
## 39. A hidden option is not a locked door in the log either
`whenLocked: 'hide'` (#37) kept "Put money against the book" off the screen of a
player who owed nothing, and it worked. But the turn record listed offered options
without saying which were hidden, so `npm run analyze` reported the option as a
locked door shown three times to a player who never saw it. The instrument was
counting the very thing the fix removed.
The turn record now carries `hidden` for every offered option, and the analyser
skips hidden options when counting locked doors. Logs exported before 0.3.4 do not
have the field and still over-count.
The same session's exports also put a save file next to the feedback log in
`logs/`. The analyser read "the newest `.json`", so whichever of the two was
exported last would have been read as the log. It now reads only
`theladder-feedback-*.json` when no file is named.
## 40. What the first Frontier playtest changed
The session (112 turns, 0.3.3, promoted on turn 44) confirmed the systems #36 and
#37 were built for: both lifestyle purchases were found and taken, the standing
charge fired as designed, money never went below zero, and the run ended flat —
the break-even the Frontier intends at the top. Everything it found wrong was
legibility, plus the defect in #38. All eight of the player's comments were
questions about what a number meant, and both thumbs-down were on events that
confused them.
- **Wages are called wages.** "Settlement" and "Deputy pay" became "Wages from
Vane" and "Wages from the county". The player could not connect "Settlement"
to the twenty-four a run the first morning quotes, and read a run's own +$9 as
the pay going missing (#27: the ledger is written for a player).
- **A clamped change says where it stopped** — "stopped at $0", "stopped at 100" —
instead of "at its limit", which was read as a balance still outstanding.
- **Two option labels were rewritten** that the player could not parse: "Have no
view on the axle" and "take hold of the nearest one".
- **The horse and the room are separate events.** One event offered both, with a
description that was always about the horse, so the room was offered under
horse text and "Buy the bay" was shown locked to a player who had bought it — a
door that could never open. Each is now gated on the tier it sells, and a test
holds that neither is offered once bought.
- **The promotion gets a warning.** "How do I find out how close I am to
promotion?" had no answer anywhere. `ward_takes_notice` comes once, to a hand
past turn 24 with standing 55, and says in the town's terms what the offer waits
for — a name the street says for the right reasons, and a man Ward takes at his
word or one with the Ridge road behind him — without printing a threshold. Across
72 archetype runs it fired in 67, at a median of turn 29, always before the offer.
A numeric progress display was not built: it would need the interface to know
what a promotion is, which #22 keeps in content.
- **The office got five more days.** 69 deputy turns saw 13 distinct situations,
and `office_day` and `the_cells` were 17% of the stage each — the same thinness
#24 caught in Dispatch. Five events were added (the stage in short, the county's
fees, a bounty hunter, a fire at the store, Dutch wanting the Ridge road) and the
horse/room split adds a sixth. Measured across 72 archetype runs of 250 turns,
the three floor events fell from about 17.5% of deputy turns each to about
13.5%. Every new event has an ungated way through, and its skill gates (nerve 30
and 34, trade 28 and 30) sit under growth available without them (#32).
**Measured and not yet understood.** In the same simulation, promoted runs now end
with a median of $164 rather than $88, and a median standing of 89 rather than 98.
Two changes plausibly account for it — repayments no longer overcharge, and lower
standing triggers the $4 standing charge less often — but it has not been
isolated. The declared baselines and every tuning test still pass. The Frontier is
meant to be roughly level at the top, so the next Frontier session should watch
whether money stops mattering again, as it did in Corporate Ladder before #36.
"Owed at the store" was asked for as "a major field at the top". It already is,
second after Money whenever anything is owed. Whether it is prominent enough can
only be judged in a browser, which this machine cannot render (#18).
## 41. Chance, and a cast that changes between runs
Recorded 2026-09-23. Designer's call: a change to the base engine, generic, which
every pack may use and none has to. Corporate Ladder and the Frontier use none of
it and replay exactly as before.
Until now an option did exactly what it said. Skills only decided whether an
option was on the menu (#15, #32). The story-to-pack catalogue of recurring
situations (asking for a rise, the door to the powerful, a colleague's needling)
is made of moments whose outcome depends on who you are and who you are dealing
with, so three things were added. `content/countinghouse` is the pack that exercises them.
**Attributes.** A pack names them in `pack.attributes`. The player's live in
ordinary declared state (`attributes.<id>`), so they grow by effects like anything
else. The prototype's four, agreed with the designer, are Nerve, Wits, Charm and
Resolve. Another pack may rename them, as #14 lets it rename the interface.
**Checks.** An option may carry `check: { attribute, against?, opposedBy?,
relationship?, difficulty?, modifier? }` and then must carry `success` and
`failure`, each with its own `result` and `effects`. Its plain `effects` apply
either way. The chance is
base + perPoint × (yours − theirs) + relationshipPerPoint × (their regard − centre) + modifier,
clamped to [floor, ceiling]
where "theirs" is the opposing character's attribute, or the check's `difficulty`,
or the pack's `unopposed`. Every number is the pack's, in `pack.checks` (#9).
**The player never sees a number.** Options show the odds in words ("a long shot",
"likely"), from bands the pack may rename, and the outcome says whether it came
off. The roll is one draw from the run's own generator, so a run with checks still
replays exactly from its seed and choices (#5, #6). A pack that never checks draws
nothing extra.
**A cast drawn per run.** A character may be a *role* with `candidates`, several
authored people who could fill it, and one is drawn when a run starts. A role may
be a *group* with `count: [min, max]` (two to four clerks), which draws that many
different people into numbered slots (`clerk_1` .. `clerk_4`). Every candidate
carries a name, a description and the same attributes the player has, so a
replayed pack meets a different office with different strengths. Candidates are
authored rather than generated from random parts: a character written as sharp
has the wits to see through a bluff, which a random attribute roll pasted onto a
random description would not guarantee.
- **The draw is saved in engine-owned state** (`cast`, now a reserved root, #4)
and made on its own stream derived from the seed, so it neither moves the event
generator nor depends on it. `hydrate` (#36) draws one for a save that predates
it, and draws the same one every time.
- **Every slot up to a group's maximum is declared in the schema from the start.**
Relationship state for `clerk_3` exists whether or not anyone was drawn into it,
so it validates and saves like any other path. An empty slot is recorded as
`null` and is simply absent from the run.
- **Text names people by role.** `{partner}`, `{chief.first}` and `{clerk.title}`
are filled in from whoever was drawn. An event about "a clerk" says
`involves: ['clerk']`. When it is drawn, one present clerk is bound to it for
the turn, and `{clerk}` in its text, `relationships.{clerk}.trust` in its effects
and `against: '{clerk}'` in a check all mean that person. The binding is saved
with the run. An event cannot fire in a run with nobody to bind. Text kept in
state, such as a notable moment, has its placeholders pinned to the slot
(`{clerk_2}`), so the retrospective names the right person later.
- **The validator holds all of it:**
- attributes declared, and every drawn person carrying all of them;
- enough candidates for a group's maximum, and unique names;
- checks naming real attributes and people, with both branches;
- `{placeholders}` that name real characters, where a group is only named inside an event that binds it;
- no placeholder in an event's own `requires`, which is evaluated before anyone is bound.
**What the test harness had to learn.** The archetypes score an option by its
effects (#21). A checked option is now scored at its expected value: plain effects
in full, and each branch weighted by its chance, the way a player reading the odds
would. The conformance check that no repayment takes more than is owed (#38) now
reads both branches, since a check can pay or spend on either.
**The prototype pack was tuned by measurement, not by eye.** Its first draft
failed three conformance tests:
- the routine ledgers were a third of every player's turns;
- a careless player was broke 40% of a run;
- it had no balance a borrower could owe.
Two more catalogue situations (an errand for the partner's family, and a desk you
and a clerk both want), a lighter floor event, a loan that is refused but still
helps a little, and an "owed to the floor" balance repaid on Saturdays brought
those to 22% at worst, 12% at worst, and a debt that is repaid by reading the
balance (#38).
**Not yet known.** Whether chance makes the same situations worth replaying, and
whether the odds in words are legible, can only be judged by someone playing it
(#18). The pack has one stage on purpose, so that question can be answered before
a career is built on it.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "theladder", "name": "theladder",
"version": "0.4.0", "version": "0.3.3",
"private": true, "private": true,
"description": "A turn-based career simulation game. Engine is theme-agnostic; settings are content packs.", "description": "A turn-based career simulation game. Engine is theme-agnostic; settings are content packs.",
"type": "module", "type": "module",
-139
View File
@@ -1,139 +0,0 @@
// Who is in this run.
//
// A pack's characters are fixed people by default: a name and a description,
// written once. A character may instead be a *role* with `candidates` — several
// authored people who could fill it — and one is drawn when a run starts. A role
// may also be a *group* with a `count` range, like two to four co-workers, in
// which case that many different candidates are drawn into numbered slots.
// Replaying a pack therefore meets a different cast, which is the point.
//
// Slots are what state is keyed by: a single role is its own id (`boss`), a
// group's members are `coworker_1` .. `coworker_<max>`. Every slot up to the
// group's maximum exists in the schema from the start, so relationship state
// is declared, validated and saved like any other; a slot nobody was drawn into
// is simply not present in the run. See DECISIONS #41.
//
// The draw uses its own stream derived from the run's seed, so it neither moves
// the event generator nor depends on it: a pack that gains a cast does not
// change which events any other pack draws, and a save that predates the cast
// can have one drawn for it on load, reproducibly.
import { createRng } from './rng.js';
import { getPath } from './paths.js';
/** The count range of a group role, or null for a single character. */
export function groupRange(character) {
if (character.count === undefined) return null;
return Array.isArray(character.count) ? character.count : [character.count, character.count];
}
/** Every state slot a character occupies: its own id, or one per possible group member. */
export function slotsOf(character) {
const range = groupRange(character);
if (!range) return [character.id];
return Array.from({ length: range[1] }, (_, i) => `${character.id}_${i + 1}`);
}
/** Whether a pack draws any part of its cast per run. */
export function hasDrawnCast(pack) {
return (pack.characters ?? []).some((c) => c.candidates?.length || groupRange(c));
}
/** The authored person behind a candidate entry, in the shape the state keeps. */
function personOf(character, candidate, index) {
return {
role: character.id,
candidate: index,
name: candidate.name,
title: candidate.title ?? character.title ?? null,
description: candidate.description ?? '',
attributes: { ...(candidate.attributes ?? {}) },
};
}
/**
* Draw the cast for a run. Returns `{ <slot>: person | null }` for every slot of
* every drawn role; a group slot left empty is `null`, which is how "not in this
* run" is recorded. Fixed characters are not in it: they need no drawing.
*/
export function drawCast(pack, seed) {
const rng = createRng(`cast:${seed}`);
const cast = {};
for (const character of pack.characters ?? []) {
const pool = character.candidates ?? [];
const range = groupRange(character);
if (!range && !pool.length) continue;
const slots = slotsOf(character);
const wanted = range ? range[0] + rng.int(range[1] - range[0] + 1) : 1;
const remaining = pool.map((candidate, index) => ({ candidate, index }));
for (let i = 0; i < slots.length; i++) {
if (i >= wanted || !remaining.length) {
cast[slots[i]] = null;
continue;
}
const [picked] = remaining.splice(rng.int(remaining.length), 1);
cast[slots[i]] = personOf(character, picked.candidate, picked.index);
}
}
return cast;
}
/** Starting relationship overrides a drawn person brings with them. */
export function drawnRelationships(pack, cast) {
const overrides = [];
for (const character of pack.characters ?? []) {
for (const slot of slotsOf(character)) {
const person = cast?.[slot];
if (!person) continue;
const candidate = character.candidates?.[person.candidate];
for (const [stat, value] of Object.entries(candidate?.startingRelationship ?? {})) {
overrides.push({ path: `relationships.${slot}.${stat}`, value });
}
}
}
return overrides;
}
/** The character definition that owns a slot. */
export function characterForSlot(pack, slot) {
for (const character of pack.characters ?? []) {
if (slotsOf(character).includes(slot)) return character;
}
return undefined;
}
/**
* The person in a slot as the interface and the checks see them: name, title,
* description and attributes, whether drawn for this run or written into the
* pack. Undefined if the slot is empty this run or unknown.
*/
export function personIn(pack, state, slot) {
const character = characterForSlot(pack, slot);
if (!character) return undefined;
const drawn = getPath(state, `cast.${slot}`);
if (drawn === null) return undefined;
if (drawn) return { id: slot, ...drawn };
if (character.candidates?.length || groupRange(character)) return undefined;
return {
id: slot,
role: character.id,
name: character.name,
title: character.title ?? null,
description: character.description ?? '',
attributes: { ...(character.attributes ?? {}) },
};
}
/** Everyone present in this run, in pack order. */
export function presentPeople(pack, state) {
return (pack.characters ?? []).flatMap((character) =>
slotsOf(character).map((slot) => personIn(pack, state, slot)).filter(Boolean));
}
/** The slots of a role that are filled in this run. */
export function presentSlots(pack, state, roleId) {
const character = (pack.characters ?? []).find((c) => c.id === roleId);
if (!character) return [];
return slotsOf(character).filter((slot) => personIn(pack, state, slot));
}
-87
View File
@@ -1,87 +0,0 @@
// Options whose outcome is not certain.
//
// An option may carry a `check`: which of the player's attributes it tests,
// optionally *against* a character who opposes it with their own attribute, and
// optionally leaning on how that character regards the player. It then has a
// `success` and a `failure`, each with its own result text and effects; the
// option's plain `effects` apply either way. The engine works out the chance,
// rolls on the run's own generator, and records which way it went.
//
// The engine knows the arithmetic and nothing about what any attribute means:
// the numbers are the pack's (`pack.checks`), the attribute names are the
// pack's, and so are the words the player sees in place of a percentage. A
// player is shown "a long shot" or "likely", never a number. See DECISIONS #41.
//
// chance = base + perPoint × (yours − theirs)
// + relationshipPerPoint × (their regard − relationshipCentre)
// + modifier, clamped to [floor, ceiling]
//
// where "theirs" is the opposing character's attribute when the check is
// `against` someone, and the check's `difficulty` (or the pack's `unopposed`)
// when it is not.
import { getPath } from './paths.js';
import { personIn } from './cast.js';
import { resolveSlot } from './text.js';
/** Odds words used when a pack does not name its own. Upper bounds, in percent. */
export const DEFAULT_ODDS = [
{ upTo: 20, words: 'a long shot' },
{ upTo: 40, words: 'unlikely' },
{ upTo: 60, words: 'an even chance' },
{ upTo: 80, words: 'likely' },
{ upTo: 100, words: 'a safe bet' },
];
/** The words for a chance, from the pack's bands. */
export function oddsWords(pack, chance) {
const bands = pack.checks?.odds ?? DEFAULT_ODDS;
return (bands.find((band) => chance <= band.upTo) ?? bands[bands.length - 1]).words;
}
/** The slot a check is made against this turn, if any. */
function opponentSlot(check, bindings) {
if (!check.against) return undefined;
const id = check.against.replace(/^\{|\}$/g, '');
return resolveSlot(id, bindings);
}
/**
* The chance of an option's check succeeding in this state, as a whole
* percentage, with the parts it was built from. Undefined for an option with
* no check.
*/
export function chanceOf(pack, state, option, bindings = state?.meta?.bindings ?? {}) {
const check = option.check;
if (!check) return undefined;
const rules = pack.checks ?? {};
const yours = getPath(state, `attributes.${check.attribute}`) ?? 0;
const slot = opponentSlot(check, bindings);
const opponent = slot ? personIn(pack, state, slot) : undefined;
const theirs = opponent
? opponent.attributes?.[check.opposedBy ?? check.attribute] ?? 0
: check.difficulty ?? rules.unopposed ?? 0;
let regard = 0;
if (check.relationship && slot) {
const value = getPath(state, `relationships.${slot}.${check.relationship}`);
if (typeof value === 'number') {
regard = (rules.relationshipPerPoint ?? 0) * (value - (rules.relationshipCentre ?? 50));
}
}
const raw = (rules.base ?? 50) + (rules.perPoint ?? 0) * (yours - theirs) + regard + (check.modifier ?? 0);
const chance = Math.round(Math.min(rules.ceiling ?? 95, Math.max(rules.floor ?? 5, raw)));
return { chance, words: oddsWords(pack, chance), yours, theirs, against: slot ?? null };
}
/**
* Roll a check. `rng` is the run's generator; exactly one number is drawn, so
* a replay from the same seed and choices rolls the same way.
*/
export function rollCheck(pack, state, option, rng, bindings) {
const odds = chanceOf(pack, state, option, bindings);
const roll = rng.next() * 100;
return { ...odds, succeeded: roll < odds.chance };
}
+5 -32
View File
@@ -25,50 +25,23 @@ function clamp(value, rule) {
return out; return out;
} }
/** Whether an effect's value is a reference to state rather than a literal. */
export function isAmountFrom(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value)
&& typeof value.from === 'string';
}
/**
* The amount an `add` effect applies. Usually the number the pack wrote. It may
* instead be read from state at the moment the effect applies:
*
* { path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 20 } }
*
* is "take away whatever is owed, but no more than 20". A fixed instalment
* cannot pay off a balance that could be anything, and charging the whole
* instalment against a smaller balance took a playtester's money for a debt
* that did not exist (DECISIONS #38). It is still data, and still generic: the
* engine reads a number from a path without knowing what the path means.
*/
export function resolveAmount(state, value) {
if (!isAmountFrom(value)) return value;
const current = getPath(state, value.from);
let amount = typeof current === 'number' ? current : 0;
if (typeof value.cap === 'number') amount = Math.min(amount, value.cap);
return amount * (typeof value.times === 'number' ? value.times : 1);
}
/** /**
* Apply one effect. `schema` is the resolved state schema, consulted only for * Apply one effect. `schema` is the resolved state schema, consulted only for
* clamping — the engine still has no idea what any given path means. * clamping — the engine still has no idea what any given path means.
*/ */
export function applyEffect(state, effect, schema = {}) { export function applyEffect(state, effect, schema = {}) {
const { path, op } = effect; const { path, op, value } = effect;
const operate = OPS[op]; const operate = OPS[op];
if (!operate) throw new TypeError(`unknown effect operation ${JSON.stringify(op)}`); if (!operate) throw new TypeError(`unknown effect operation ${JSON.stringify(op)}`);
const value = op === 'add' ? resolveAmount(state, effect.value) : effect.value;
const from = getPath(state, path); const from = getPath(state, path);
const raw = operate(from, value); const raw = operate(from, value);
const to = clamp(raw, schema[path]); const to = clamp(raw, schema[path]);
const change = { path, op, value, from, to, clamped: raw !== to }; return {
// The log keeps the number actually applied, and says where it came from. state: setPath(state, path, to),
if (value !== effect.value) change.source = effect.value; change: { path, op, value, from, to, clamped: raw !== to },
return { state: setPath(state, path, to), change }; };
} }
/** Apply a list of effects in order. Returns the new state and every change. */ /** Apply a list of effects in order. Returns the new state and every change. */
+4 -40
View File
@@ -9,10 +9,8 @@ import { createRng } from './rng.js';
import { applyEffects } from './effects.js'; import { applyEffects } from './effects.js';
import { check } from './conditions.js'; import { check } from './conditions.js';
import { resolveSchema, createState } from './state.js'; import { resolveSchema, createState } from './state.js';
import { selectEvent, resolveOptions, findOption, bindRoles } from './select.js'; import { selectEvent, resolveOptions, findOption } from './select.js';
import { setPath, getPath } from './paths.js'; import { setPath, getPath } from './paths.js';
import { chanceOf, rollCheck } from './checks.js';
import { bindEffects, fillText } from './text.js';
export class ContentError extends Error {} export class ContentError extends Error {}
export class MoveError extends Error {} export class MoveError extends Error {}
@@ -30,10 +28,6 @@ function advance(pack, state) {
); );
} }
let next = setPath(dated, 'meta.currentEventId', event.id); let next = setPath(dated, 'meta.currentEventId', event.id);
// Which co-worker this event is about, drawn after the event itself. A pack
// with no `involves` draws nothing here, so its runs are unchanged.
const bindings = bindRoles(pack, next, event, rng);
if (Object.keys(bindings).length || next.meta.bindings) next = setPath(next, 'meta.bindings', bindings);
next = setPath(next, 'meta.rngPosition', rng.getPosition()); next = setPath(next, 'meta.rngPosition', rng.getPosition());
return next; return next;
} }
@@ -107,10 +101,7 @@ export function currentEvent(pack, state) {
/** The current event together with each option's availability. */ /** The current event together with each option's availability. */
export function currentTurn(pack, state) { export function currentTurn(pack, state) {
const event = currentEvent(pack, state); const event = currentEvent(pack, state);
const options = resolveOptions(state, event).map((resolved) => (resolved.option.check return { event, options: resolveOptions(state, event) };
? { ...resolved, odds: chanceOf(pack, state, resolved.option) }
: resolved));
return { event, options, bindings: state.meta.bindings ?? {} };
} }
/** /**
@@ -129,27 +120,7 @@ export function takeTurn(pack, state, optionId, { schema = resolveSchema(pack),
throw new MoveError(`option "${optionId}" is locked: ${JSON.stringify(chosen.failed)}`); throw new MoveError(`option "${optionId}" is locked: ${JSON.stringify(chosen.failed)}`);
} }
const bindings = state.meta.bindings ?? {}; const applied = applyEffects(state, option.effects, schema);
let effects = bindEffects(option.effects, bindings);
let check = null;
let rngState = state;
if (option.check) {
// One number from the run's own generator, so the roll replays with the run.
const rng = createRng(state.meta.seed, state.meta.rngPosition ?? undefined);
const rolled = rollCheck(pack, state, option, rng, bindings);
const outcome = rolled.succeeded ? option.success : option.failure;
effects = [...effects, ...bindEffects(outcome?.effects, bindings)];
check = {
attribute: option.check.attribute,
against: rolled.against,
chance: rolled.chance,
odds: rolled.words,
outcome: rolled.succeeded ? 'success' : 'failure',
};
rngState = setPath(state, 'meta.rngPosition', rng.getPosition());
}
const applied = applyEffects(rngState, effects, schema);
let next = applied.state; let next = applied.state;
const seenCount = getPath(next, `seen.${event.id}`) ?? 0; const seenCount = getPath(next, `seen.${event.id}`) ?? 0;
@@ -164,10 +135,7 @@ export function takeTurn(pack, state, optionId, { schema = resolveSchema(pack),
stage: next.stage, stage: next.stage,
eventId: event.id, eventId: event.id,
optionId: option.id, optionId: option.id,
// Names filled in, so the log and the retrospective read as the player saw it. optionLabel: option.label,
optionLabel: fillText(pack, state, option.label, bindings),
...(check ? { check } : {}),
...(Object.keys(bindings).length ? { bindings } : {}),
changes: applied.changes, changes: applied.changes,
upkeep: upkeep.changes, upkeep: upkeep.changes,
offered: resolved.map((r) => ({ offered: resolved.map((r) => ({
@@ -175,10 +143,6 @@ export function takeTurn(pack, state, optionId, { schema = resolveSchema(pack),
label: r.option.label, label: r.option.label,
available: r.available, available: r.available,
failed: r.failed, failed: r.failed,
// A hidden option was never on the player's screen. Without this the log
// cannot tell it from a locked door they saw, and the analyser counted
// one the first real Frontier player never met. See DECISIONS #39.
hidden: r.hidden,
})), })),
at: now, at: now,
}; };
-3
View File
@@ -7,6 +7,3 @@ export { createState, resolveSchema, indexCharacters, RESERVED_ROOTS } from './s
export { eligibleEvents, selectEvent, resolveOptions, findOption } from './select.js'; export { eligibleEvents, selectEvent, resolveOptions, findOption } from './select.js';
export { startGame, currentEvent, currentTurn, takeTurn, ContentError, MoveError } from './game.js'; export { startGame, currentEvent, currentTurn, takeTurn, ContentError, MoveError } from './game.js';
export { validatePack, assertValidPack } from './validate.js'; export { validatePack, assertValidPack } from './validate.js';
export { drawCast, presentPeople, presentSlots, personIn, slotsOf, hasDrawnCast } from './cast.js';
export { fillText, placeholders, bindEffects, bindConditions } from './text.js';
export { chanceOf, rollCheck, oddsWords, DEFAULT_ODDS } from './checks.js';
+1 -22
View File
@@ -9,21 +9,6 @@
import { check } from './conditions.js'; import { check } from './conditions.js';
import { getPath } from './paths.js'; import { getPath } from './paths.js';
import { presentSlots } from './cast.js';
import { bindConditions } from './text.js';
/**
* Pick, for each role an event `involves`, one person present in this run. An
* event about "a co-worker" is about one particular co-worker for the turn.
*/
export function bindRoles(pack, state, event, rng) {
const bindings = {};
for (const role of event.involves ?? []) {
const slots = presentSlots(pack, state, role);
if (slots.length) bindings[role] = slots.length === 1 ? slots[0] : rng.pick(slots);
}
return bindings;
}
/** Events that could fire right now, with the reason excluded ones could not. */ /** Events that could fire right now, with the reason excluded ones could not. */
export function eligibleEvents(pack, state) { export function eligibleEvents(pack, state) {
@@ -52,11 +37,6 @@ export function eligibleEvents(pack, state) {
excluded.push({ event, reason: 'requires', failed: gate.failed }); excluded.push({ event, reason: 'requires', failed: gate.failed });
continue; continue;
} }
// An event about a co-worker cannot fire in a run that drew none.
if ((event.involves ?? []).some((role) => !presentSlots(pack, state, role).length)) {
excluded.push({ event, reason: 'involves' });
continue;
}
eligible.push(event); eligible.push(event);
} }
@@ -80,9 +60,8 @@ export function selectEvent(pack, state, rng) {
* strongest signal that the choice set is too narrow. * strongest signal that the choice set is too narrow.
*/ */
export function resolveOptions(state, event) { export function resolveOptions(state, event) {
const bindings = state?.meta?.bindings ?? {};
return (event.options ?? []).map((option) => { return (event.options ?? []).map((option) => {
const gate = check(state, bindConditions(option.requires, bindings)); const gate = check(state, option.requires);
return { return {
option, option,
available: gate.passed, available: gate.passed,
+3 -23
View File
@@ -5,7 +5,6 @@
// names no stat of its own beyond its own bookkeeping. // names no stat of its own beyond its own bookkeeping.
import { getPath, setPath } from './paths.js'; import { getPath, setPath } from './paths.js';
import { slotsOf, drawCast, drawnRelationships, hasDrawnCast } from './cast.js';
/** /**
* Paths the engine owns. Content may *read* these in conditions — "only after * Paths the engine owns. Content may *read* these in conditions — "only after
@@ -13,7 +12,7 @@ import { slotsOf, drawCast, drawnRelationships, hasDrawnCast } from './cast.js';
* them, which is what keeps engine bookkeeping from being corrupted by data. * them, which is what keeps engine bookkeeping from being corrupted by data.
*/ */
export const RESERVED_ROOTS = export const RESERVED_ROOTS =
new Set(['turn', 'stage', 'seen', 'lastSeen', 'history', 'meta', 'calendar', 'cast']); new Set(['turn', 'stage', 'seen', 'lastSeen', 'history', 'meta', 'calendar']);
/** /**
* Flatten a pack's state declarations into one path -> rule map, expanding the * Flatten a pack's state declarations into one path -> rule map, expanding the
@@ -28,30 +27,17 @@ export function resolveSchema(pack) {
schema[path] = { ...rule }; schema[path] = { ...rule };
} }
for (const character of pack.characters ?? []) { for (const character of pack.characters ?? []) {
// A group role has one slot per possible member (coworker_1 .. coworker_4),
// all declared up front so every one is validated and saved. See cast.js.
for (const slot of slotsOf(character)) {
for (const [stat, rule] of Object.entries(pack.relationshipStats ?? {})) { for (const [stat, rule] of Object.entries(pack.relationshipStats ?? {})) {
// A character may start somewhere other than the pack default — a rival // A character may start somewhere other than the pack default — a rival
// who already resents you, a boss who has not formed an opinion. // who already resents you, a boss who has not formed an opinion.
const override = character.startingRelationship?.[stat]; const override = character.startingRelationship?.[stat];
schema[`relationships.${slot}.${stat}`] = schema[`relationships.${character.id}.${stat}`] =
override === undefined ? { ...rule } : { ...rule, initial: override }; override === undefined ? { ...rule } : { ...rule, initial: override };
} }
} }
}
return schema; return schema;
} }
/** Draw a run's cast into state, with the starting relationships the drawn people bring. */
function withCast(pack, state) {
if (!hasDrawnCast(pack)) return state;
const cast = drawCast(pack, state.meta.seed);
let next = setPath(state, 'cast', cast);
for (const { path, value } of drawnRelationships(pack, cast)) next = setPath(next, path, value);
return next;
}
/** A fresh game state for `pack`, seeded for reproducibility. */ /** A fresh game state for `pack`, seeded for reproducibility. */
export function createState(pack, { seed, schema = resolveSchema(pack) } = {}) { export function createState(pack, { seed, schema = resolveSchema(pack) } = {}) {
let state = { let state = {
@@ -72,7 +58,7 @@ export function createState(pack, { seed, schema = resolveSchema(pack) } = {}) {
for (const [path, rule] of Object.entries(schema)) { for (const [path, rule] of Object.entries(schema)) {
state = setPath(state, path, structuredClone(rule.initial)); state = setPath(state, path, structuredClone(rule.initial));
} }
return withCast(pack, state); return state;
} }
/** /**
@@ -96,12 +82,6 @@ export function hydrate(pack, state, schema = resolveSchema(pack)) {
next = setPath(next, path, structuredClone(rule.initial)); next = setPath(next, path, structuredClone(rule.initial));
added.push(path); added.push(path);
} }
// A run saved before its pack drew a cast gets one now, from its own seed, so
// the same save always wakes up with the same people.
if (hasDrawnCast(pack) && next.cast === undefined) {
next = withCast(pack, next);
added.push('cast');
}
return { state: next, added }; return { state: next, added };
} }
-77
View File
@@ -1,77 +0,0 @@
// Names in text, and which person an event is about.
//
// Content that meets a different cast each run cannot write "Dutch" into its
// prose. It writes `{rival}`, and the name is filled in from whoever was drawn.
// `{rival.first}` is the first name, `{rival.title}` the title.
//
// A group role (two to four co-workers) cannot be named by its role alone —
// which one? An event says so with `involves: ['coworker']`, and when the event
// is drawn one present member is bound to it for the turn. `{coworker}` in its
// text, `relationships.{coworker}.trust` in its effects and `against:
// '{coworker}'` in a check all mean that person. The binding is saved with the
// run, so a replay binds the same person. See DECISIONS #41.
import { personIn } from './cast.js';
const PLACEHOLDER = /\{([A-Za-z0-9_]+)(?:\.(name|first|title))?\}/g;
/** Every `{id}` / `{id.part}` in a piece of text, as `{ id, part }`. */
export function placeholders(text) {
if (typeof text !== 'string') return [];
return [...text.matchAll(PLACEHOLDER)].map((m) => ({ id: m[1], part: m[2] ?? 'name' }));
}
/** The slot a placeholder id means this turn: a bound group member, or the id itself. */
export function resolveSlot(id, bindings = {}) {
return bindings[id] ?? id;
}
/** Replace `{id}` in a path or a string with the slot bound to it this turn. */
export function bindPath(path, bindings = {}) {
if (typeof path !== 'string') return path;
return path.replace(/\{([A-Za-z0-9_]+)\}/g, (whole, id) => resolveSlot(id, bindings));
}
/** Fill names into text. Anything it cannot resolve is left as written. */
export function fillText(pack, state, text, bindings = state?.meta?.bindings ?? {}) {
if (typeof text !== 'string') return text;
return text.replace(PLACEHOLDER, (whole, id, part = 'name') => {
const person = personIn(pack, state, resolveSlot(id, bindings));
if (!person) return whole;
if (part === 'first') return person.name.split(' ')[0];
if (part === 'title') return person.title ?? '';
return person.name;
});
}
/** Conditions with any bound placeholders in their paths replaced by the slot. */
export function bindConditions(conditions, bindings = {}) {
if (!conditions || !Object.keys(bindings).length) return conditions;
const walk = (c) => {
if (!c || typeof c !== 'object') return c;
if (Array.isArray(c)) return c.map(walk);
if ('all' in c) return { ...c, all: c.all.map(walk) };
if ('any' in c) return { ...c, any: c.any.map(walk) };
if ('not' in c) return { ...c, not: walk(c.not) };
return { ...c, path: bindPath(c.path, bindings) };
};
return walk(conditions);
}
/** Effects with any bound placeholders in their paths replaced by the slot. */
export function bindEffects(effects, bindings = {}) {
return (effects ?? []).map((effect) => {
const path = bindPath(effect.path, bindings);
let value = effect.value;
if (value && typeof value === 'object' && typeof value.from === 'string') {
value = { ...value, from: bindPath(value.from, bindings) };
} else if (typeof value === 'string') {
// Text kept in state (a notable moment) outlives the turn's bindings, so
// `{clerk}` is pinned to the slot it meant: `{clerk_2}` still names the
// same person when the retrospective reads it back.
value = value.replace(PLACEHOLDER, (whole, id, part) =>
(bindings[id] ? `{${bindings[id]}${part ? `.${part}` : ''}}` : whole));
}
return path === effect.path && value === effect.value ? effect : { ...effect, path, value };
});
}
+10 -189
View File
@@ -6,10 +6,8 @@
import { RESERVED_ROOTS, resolveSchema, indexCharacters } from './state.js'; import { RESERVED_ROOTS, resolveSchema, indexCharacters } from './state.js';
import { OPERATORS, pathsUsed as conditionPaths } from './conditions.js'; import { OPERATORS, pathsUsed as conditionPaths } from './conditions.js';
import { OPERATIONS, isAmountFrom } from './effects.js'; import { OPERATIONS } from './effects.js';
import { parsePath } from './paths.js'; import { parsePath } from './paths.js';
import { groupRange } from './cast.js';
import { placeholders } from './text.js';
const ID = /^[A-Za-z0-9_]+$/; const ID = /^[A-Za-z0-9_]+$/;
@@ -42,108 +40,14 @@ export function validatePack(pack) {
} }
} }
// Attributes are ordinary declared state (`attributes.<id>` for the player);
// pack.attributes names them, so checks and characters can be held to the list.
const attributeIds = new Set(Object.keys(pack.attributes ?? {}));
for (const id of attributeIds) {
if (!ID.test(id)) fail(`attribute "${id}"`, `id must match ${ID}`);
if (!(`attributes.${id}` in schema)) fail(`attribute "${id}"`, `the player needs "attributes.${id}" in pack.state`);
}
const checkAttributes = (where, attributes, { complete }) => {
for (const key of Object.keys(attributes ?? {})) {
if (!attributeIds.has(key)) fail(where, `attribute "${key}" is not in pack.attributes`);
else if (typeof attributes[key] !== 'number') fail(where, `attribute "${key}" must be a number`);
}
if (complete) {
for (const id of attributeIds) {
if (typeof attributes?.[id] !== 'number') fail(where, `has no "${id}", which every drawn person needs`);
}
}
};
const seenCharacters = new Set(); const seenCharacters = new Set();
const names = new Map();
const nameOnce = (where, name) => {
if (names.has(name)) fail(where, `name "${name}" is already used by ${names.get(name)}`);
else names.set(name, where);
};
for (const character of pack.characters ?? []) { for (const character of pack.characters ?? []) {
const where = `character "${character.id}"`; const where = `character "${character.id}"`;
if (!character.id || !ID.test(character.id)) fail(where, `id must match ${ID}`); if (!character.id || !ID.test(character.id)) fail(where, `id must match ${ID}`);
if (seenCharacters.has(character.id)) fail(where, 'duplicate id'); if (seenCharacters.has(character.id)) fail(where, 'duplicate id');
seenCharacters.add(character.id); seenCharacters.add(character.id);
if (!character.role_type) warn(where, 'no role_type tag; content cannot filter on it');
const range = groupRange(character);
const pool = character.candidates ?? [];
if (range) {
const [min, max] = range;
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max < 1 || min > max) {
fail(where, `count ${JSON.stringify(character.count)} must be [min, max] whole numbers with 0 <= min <= max and max >= 1`);
} else if (pool.length < max) {
fail(where, `can hold up to ${max} people but has only ${pool.length} candidates`);
}
}
if (range || pool.length) {
if (character.name) warn(where, 'has a name as well as candidates; the drawn candidate\'s name is used');
pool.forEach((candidate, index) => {
const at = `${where} candidate ${index}`;
if (!candidate.name) fail(at, 'name is required');
else nameOnce(at, candidate.name);
if (!candidate.description) warn(at, 'no description');
checkAttributes(at, candidate.attributes, { complete: attributeIds.size > 0 });
for (const stat of Object.keys(candidate.startingRelationship ?? {})) {
if (!(stat in (pack.relationshipStats ?? {}))) fail(at, `startingRelationship names unknown stat "${stat}"`);
}
});
} else {
if (!character.name) fail(where, 'name is required'); if (!character.name) fail(where, 'name is required');
else nameOnce(where, character.name); if (!character.role_type) warn(where, 'no role_type tag; content cannot filter on it');
checkAttributes(where, character.attributes, { complete: false });
}
}
const characterById = Object.fromEntries((pack.characters ?? []).map((c) => [c.id, c]));
const isGroup = (id) => Boolean(characterById[id] && groupRange(characterById[id]));
/**
* A path or text may name a character as `{id}`. A single character is always
* there to name; a group member only when the event binds one with `involves`.
* Returns the path with each placeholder replaced by a representative slot, so
* it can be checked against the schema like any written path.
*/
const bindForCheck = (where, text, involves) => {
for (const { id } of placeholders(text)) {
if (!characterById[id]) fail(where, `{${id}} names no character`);
else if (isGroup(id) && !involves.includes(id)) {
fail(where, `{${id}} is a group; the event must list "${id}" in involves to say which one`);
}
}
return typeof text === 'string'
? text.replace(/\{([A-Za-z0-9_]+)\}/g, (w, id) => (isGroup(id) ? `${id}_1` : id))
: text;
};
// Chances are the pack's numbers, like every other tuning value (#9).
const usesChecks = (pack.events ?? []).some((e) => (e.options ?? []).some((o) => o.check));
if (usesChecks) {
const rules = pack.checks;
if (!rules) fail('checks', 'options carry checks but the pack declares no checks block');
else {
for (const key of ['base', 'perPoint', 'floor', 'ceiling']) {
if (typeof rules[key] !== 'number') fail('checks', `${key} must be a number`);
}
if (rules.floor > rules.ceiling) fail('checks', 'floor is above ceiling');
if (rules.odds) {
let last = -Infinity;
for (const band of rules.odds) {
if (typeof band.upTo !== 'number' || band.upTo <= last) fail('checks', 'odds bands must rise by upTo');
if (!band.words) fail('checks', 'every odds band needs words');
last = band.upTo;
}
if (last < (rules.ceiling ?? 100)) fail('checks', `odds bands stop at ${last}, below the ceiling`);
}
}
} }
const readable = (path) => { const readable = (path) => {
@@ -260,31 +164,6 @@ export function validatePack(pack) {
fail(where, `weight ${event.weight} must be greater than zero, or the event can never fire`); fail(where, `weight ${event.weight} must be greater than zero, or the event can never fire`);
} }
checkConditions(where, event.requires); checkConditions(where, event.requires);
const involves = event.involves ?? [];
for (const role of involves) {
if (!characterById[role]) fail(where, `involves "${role}", which is no character`);
}
// Bindings are drawn after the event is chosen, so the event's own gate
// cannot depend on who they are.
const gatePaths = event.requires
? (Array.isArray(event.requires) ? event.requires : [event.requires]).flatMap((c) => conditionPaths(c))
: [];
if (gatePaths.some((p) => p.includes('{'))) {
fail(where, 'requires names a {character}; only options and effects can, once the event is drawn');
}
bindForCheck(where, event.description, involves);
const bound = (conditions) => {
if (!conditions) return conditions;
const walk = (c) => {
if (!c || typeof c !== 'object') return c;
if (Array.isArray(c)) return c.map(walk);
if ('all' in c) return { ...c, all: c.all.map(walk) };
if ('any' in c) return { ...c, any: c.any.map(walk) };
if ('not' in c) return { ...c, not: walk(c.not) };
return { ...c, path: bindForCheck(where, c.path, involves) };
};
return walk(conditions);
};
// A fallback must be one that *no* exclusion rule can remove: requires, // A fallback must be one that *no* exclusion rule can remove: requires,
// once and cooldown are exactly the three reasons eligibleEvents drops an // once and cooldown are exactly the three reasons eligibleEvents drops an
// event, so a floor event may carry none of them. // event, so a floor event may carry none of them.
@@ -301,78 +180,20 @@ export function validatePack(pack) {
if (seenOptions.has(option.id)) fail(optionWhere, 'duplicate id'); if (seenOptions.has(option.id)) fail(optionWhere, 'duplicate id');
seenOptions.add(option.id); seenOptions.add(option.id);
if (!option.label) fail(optionWhere, 'label is required'); if (!option.label) fail(optionWhere, 'label is required');
bindForCheck(optionWhere, option.label, involves); checkConditions(optionWhere, option.requires);
bindForCheck(optionWhere, option.result, involves);
checkConditions(optionWhere, bound(option.requires));
const checkEffects = (at, effects) => { for (const effect of option.effects ?? []) {
for (const effect of effects ?? []) {
if (!OPERATIONS.includes(effect.op)) { if (!OPERATIONS.includes(effect.op)) {
fail(at, `unknown effect operation ${JSON.stringify(effect.op)}`); fail(optionWhere, `unknown effect operation ${JSON.stringify(effect.op)}`);
} }
if (effect.op === 'add' && typeof effect.value !== 'number') { if (['add'].includes(effect.op) && typeof effect.value !== 'number') {
if (isAmountFrom(effect.value)) { fail(optionWhere, `effect "${effect.path}" uses ${effect.op} with a non-numeric value`);
// An amount read from state must read something the pack declares,
// and scale and cap it by numbers. See DECISIONS #38.
const problem = readable(bindForCheck(at, effect.value.from, involves));
if (problem) fail(at, `effect "${effect.path}" reads its amount from ${problem}`);
for (const key of ['times', 'cap']) {
if (effect.value[key] !== undefined && typeof effect.value[key] !== 'number') {
fail(at, `effect "${effect.path}" has a non-numeric ${key}`);
} }
} }
} else { for (const effect of option.effects ?? []) {
fail(at, `effect "${effect.path}" uses ${effect.op} with a non-numeric value`);
}
}
}
for (const effect of effects ?? []) {
if (typeof effect.path !== 'string') continue; if (typeof effect.path !== 'string') continue;
const problem = writable(bindForCheck(at, effect.path, involves), effect, at); const problem = writable(effect.path, effect, optionWhere);
if (problem) fail(at, `effect writes ${problem}`); if (problem) fail(optionWhere, `effect writes ${problem}`);
}
};
checkEffects(optionWhere, option.effects);
if (option.check) {
const c = option.check;
const at = `${optionWhere} check`;
if (!attributeIds.has(c.attribute)) fail(at, `attribute "${c.attribute}" is not in pack.attributes`);
if (c.opposedBy !== undefined && !attributeIds.has(c.opposedBy)) {
fail(at, `opposedBy "${c.opposedBy}" is not in pack.attributes`);
}
if (c.against !== undefined) {
const id = String(c.against).replace(/^\{|\}$/g, '');
const character = characterById[id];
if (!character) fail(at, `against "${c.against}" is no character`);
else {
if (isGroup(id) && !involves.includes(id)) {
fail(at, `against "${c.against}" is a group; the event must list "${id}" in involves`);
}
const attribute = c.opposedBy ?? c.attribute;
const people = character.candidates?.length ? character.candidates : [character];
if (people.some((p) => typeof p.attributes?.[attribute] !== 'number')) {
fail(at, `not everyone who can be "${id}" has "${attribute}" to oppose with`);
}
}
} else if (c.relationship !== undefined) {
fail(at, 'relationship needs against: whose regard is it?');
}
if (c.relationship !== undefined && !(c.relationship in (pack.relationshipStats ?? {}))) {
fail(at, `relationship "${c.relationship}" is not in relationshipStats`);
}
for (const key of ['difficulty', 'modifier']) {
if (c[key] !== undefined && typeof c[key] !== 'number') fail(at, `${key} must be a number`);
}
for (const branch of ['success', 'failure']) {
const outcome = option[branch];
if (!outcome) { fail(at, `has no ${branch}; a check needs both ways it can go`); continue; }
if (!outcome.result) warn(`${optionWhere} ${branch}`, 'no result text; the player will not be told');
bindForCheck(`${optionWhere} ${branch}`, outcome.result, involves);
checkEffects(`${optionWhere} ${branch}`, outcome.effects);
}
} else if (option.success || option.failure) {
fail(optionWhere, 'has success or failure but no check to decide between them');
} }
} }
} }
+5 -11
View File
@@ -4,7 +4,6 @@ import { h } from './dom.js';
import { formatValue, labelFor, headerStats } from './format.js'; import { formatValue, labelFor, headerStats } from './format.js';
import { APP, versionLine } from '../version.js'; import { APP, versionLine } from '../version.js';
import { getPath } from '../engine/paths.js'; import { getPath } from '../engine/paths.js';
import { fillText } from '../engine/text.js';
/** /**
* What you are playing, which pack, and who you currently are — plus the build, * What you are playing, which pack, and who you currently are — plus the build,
@@ -58,14 +57,12 @@ export function statPanel(pack, state, schema) {
/** One character, with how they currently regard the player. */ /** One character, with how they currently regard the player. */
export function characterCard(pack, state, schema, character) { export function characterCard(pack, state, schema, character) {
// `character` is a person as presentPeople() returns them: drawn for this run
// or written into the pack, keyed by the slot their relationships live under.
return h('article', { class: 'person' }, return h('article', { class: 'person' },
h('header', { class: 'person__head' }, h('header', { class: 'person__head' },
h('h3', { class: 'person__name' }, character.name), h('h3', { class: 'person__name' }, character.name),
character.title && h('p', { class: 'person__title' }, character.title), character.title && h('p', { class: 'person__title' }, character.title),
), ),
h('p', { class: 'person__description' }, fillText(pack, state, character.description)), h('p', { class: 'person__description' }, character.description),
h('div', { class: 'person__meters' }, h('div', { class: 'person__meters' },
Object.keys(pack.relationshipStats ?? {}).map((stat) => { Object.keys(pack.relationshipStats ?? {}).map((stat) => {
const path = `relationships.${character.id}.${stat}`; const path = `relationships.${character.id}.${stat}`;
@@ -81,7 +78,7 @@ export function characterCard(pack, state, schema, character) {
} }
/** The change list shown after a choice: what moved, and by how much. */ /** The change list shown after a choice: what moved, and by how much. */
export function changeList(pack, changes, { className = 'changes', state } = {}) { export function changeList(pack, changes, { className = 'changes' } = {}) {
const meaningful = changes.filter((change) => { const meaningful = changes.filter((change) => {
if (change.from === change.to) return false; if (change.from === change.to) return false;
// Some state is bookkeeping the player has no use for — `notable` is the // Some state is bookkeeping the player has no use for — `notable` is the
@@ -98,7 +95,7 @@ export function changeList(pack, changes, { className = 'changes', state } = {})
if (typeof change.to === 'boolean') { if (typeof change.to === 'boolean') {
const rule = pack.display?.[change.path]; const rule = pack.display?.[change.path];
const phrase = (change.to ? rule?.whenTrue : rule?.whenFalse) const phrase = (change.to ? rule?.whenTrue : rule?.whenFalse)
?? `${labelFor(pack, change.path, state)}: ${change.to ? 'yes' : 'no longer'}`; ?? `${labelFor(pack, change.path)}: ${change.to ? 'yes' : 'no longer'}`;
return h('li', { class: `change change--note change--${change.to ? 'up' : 'down'}` }, return h('li', { class: `change change--note change--${change.to ? 'up' : 'down'}` },
h('span', { class: 'change__phrase' }, phrase), h('span', { class: 'change__phrase' }, phrase),
); );
@@ -108,12 +105,9 @@ export function changeList(pack, changes, { className = 'changes', state } = {})
return h('li', { class: `change change--${rising ? 'up' : 'down'}` }, return h('li', { class: `change change--${rising ? 'up' : 'down'}` },
// Upkeep changes carry the label of the entry that caused them — // Upkeep changes carry the label of the entry that caused them —
// "Rent", "Wages" — which is far more use than the path's own name. // "Rent", "Wages" — which is far more use than the path's own name.
h('span', { class: 'change__label' }, change.label ?? labelFor(pack, change.path, state)), h('span', { class: 'change__label' }, change.label ?? labelFor(pack, change.path)),
h('span', { class: 'change__delta' }, deltaText(pack, change)), h('span', { class: 'change__delta' }, deltaText(pack, change)),
// Say where it stopped. "−$4 at its limit" was read by a real player as change.clamped && h('span', { class: 'change__note' }, 'at its limit'),
// a debt of four still standing, when it meant the balance hit zero.
change.clamped && h('span', { class: 'change__note' },
`stopped at ${formatValue(pack, change.path, change.to)}`),
); );
}), }),
); );
+12 -18
View File
@@ -8,7 +8,6 @@
import { getPath } from '../engine/paths.js'; import { getPath } from '../engine/paths.js';
import { check } from '../engine/conditions.js'; import { check } from '../engine/conditions.js';
import { personIn } from '../engine/cast.js';
const titleCase = (text) => const titleCase = (text) =>
text.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase()); text.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase());
@@ -18,21 +17,16 @@ export function displayFor(pack, path) {
return pack.display?.[path] ?? null; return pack.display?.[path] ?? null;
} }
/** /** A human name for a path, derived from the path when the pack is silent. */
* A human name for a path, derived from the path when the pack is silent. export function labelFor(pack, path) {
* Given the run's state, a relationship path names whoever is in that slot this
* run — a drawn boss, the second of three co-workers — not the role.
*/
export function labelFor(pack, path, state) {
const declared = displayFor(pack, path)?.label; const declared = displayFor(pack, path)?.label;
if (declared) return declared; if (declared) return declared;
const segments = path.split('.'); const segments = path.split('.');
if (segments[0] === 'relationships' && segments.length === 3) { if (segments[0] === 'relationships' && segments.length === 3) {
const name = (state && personIn(pack, state, segments[1])?.name) const character = pack.characters?.find((c) => c.id === segments[1]);
?? pack.characters?.find((c) => c.id === segments[1])?.name;
const stat = pack.relationshipStats?.[segments[2]]?.label ?? titleCase(segments[2]); const stat = pack.relationshipStats?.[segments[2]]?.label ?? titleCase(segments[2]);
return `${name ?? titleCase(segments[1])} — ${stat.toLowerCase()}`; return `${character?.name ?? titleCase(segments[1])} — ${stat.toLowerCase()}`;
} }
return titleCase(segments.at(-1)); return titleCase(segments.at(-1));
} }
@@ -67,15 +61,15 @@ export function formatDelta(pack, path, from, to) {
* One condition as a requirement the player can read: "Negotiation 15+", * One condition as a requirement the player can read: "Negotiation 15+",
* "Trevor — likes you 48+", "not in debt". * "Trevor — likes you 48+", "not in debt".
*/ */
export function describeCondition(pack, condition, state) { export function describeCondition(pack, condition) {
if (!condition || typeof condition !== 'object') return ''; if (!condition || typeof condition !== 'object') return '';
if (Array.isArray(condition)) return condition.map((c) => describeCondition(pack, c, state)).join(', '); if (Array.isArray(condition)) return condition.map((c) => describeCondition(pack, c)).join(', ');
if (condition.all) return condition.all.map((c) => describeCondition(pack, c, state)).join(', '); if (condition.all) return condition.all.map((c) => describeCondition(pack, c)).join(', ');
if (condition.any) return condition.any.map((c) => describeCondition(pack, c, state)).join(' or '); if (condition.any) return condition.any.map((c) => describeCondition(pack, c)).join(' or ');
if (condition.not) return `not ${describeCondition(pack, condition.not, state)}`; if (condition.not) return `not ${describeCondition(pack, condition.not)}`;
const { path, op, value } = condition; const { path, op, value } = condition;
const label = labelFor(pack, path, state); const label = labelFor(pack, path);
if (typeof value === 'boolean') return value ? label.toLowerCase() : `no ${label.toLowerCase()}`; if (typeof value === 'boolean') return value ? label.toLowerCase() : `no ${label.toLowerCase()}`;
@@ -92,8 +86,8 @@ export function describeCondition(pack, condition, state) {
} }
/** What a locked option is waiting for. */ /** What a locked option is waiting for. */
export function requirementText(pack, failedConditions, state) { export function requirementText(pack, failedConditions) {
const parts = failedConditions.map((c) => describeCondition(pack, c, state)).filter(Boolean); const parts = failedConditions.map((c) => describeCondition(pack, c)).filter(Boolean);
return parts.length ? `Needs ${parts.join(', ')}` : 'Not available yet'; return parts.length ? `Needs ${parts.join(', ')}` : 'Not available yet';
} }
+2 -4
View File
@@ -6,8 +6,6 @@
import { h } from '../dom.js'; import { h } from '../dom.js';
import { statPanel, characterCard, statusBar } from '../components.js'; import { statPanel, characterCard, statusBar } from '../components.js';
import { presentPeople } from '../../engine/cast.js';
import { fillText } from '../../engine/text.js';
import { formatMoney, labelFor } from '../format.js'; import { formatMoney, labelFor } from '../format.js';
import { summarize } from '../summary.js'; import { summarize } from '../summary.js';
@@ -60,14 +58,14 @@ export function retrospectiveScreen(pack, state, schema, actions) {
h('section', { class: 'retro__section' }, h('section', { class: 'retro__section' },
h('h2', { class: 'retro__heading' }, 'Where you stand with people'), h('h2', { class: 'retro__heading' }, 'Where you stand with people'),
h('div', { class: 'retro__people' }, h('div', { class: 'retro__people' },
presentPeople(pack, state).map((person) => characterCard(pack, state, schema, person)), (pack.characters ?? []).map((character) => characterCard(pack, state, schema, character)),
), ),
), ),
h('section', { class: 'retro__section' }, h('section', { class: 'retro__section' },
h('h2', { class: 'retro__heading' }, 'Notable moments'), h('h2', { class: 'retro__heading' }, 'Notable moments'),
summary.notable.length summary.notable.length
? h('ol', { class: 'moments' }, summary.notable.map((moment) => h('li', { class: 'moment' }, fillText(pack, state, moment)))) ? h('ol', { class: 'moments' }, summary.notable.map((moment) => h('li', { class: 'moment' }, moment)))
: h('p', { class: 'moments moments--none' }, : h('p', { class: 'moments moments--none' },
'Nothing that anybody would write down. That happens.'), 'Nothing that anybody would write down. That happens.'),
), ),
+13 -28
View File
@@ -12,8 +12,6 @@
import { h } from '../dom.js'; import { h } from '../dom.js';
import { statPanel, characterCard, changeList, statusBar } from '../components.js'; import { statPanel, characterCard, changeList, statusBar } from '../components.js';
import { requirementText, turnHeading, upcomingUpkeep } from '../format.js'; import { requirementText, turnHeading, upcomingUpkeep } from '../format.js';
import { presentPeople } from '../../engine/cast.js';
import { fillText } from '../../engine/text.js';
export function turnScreen(pack, state, schema, view, actions) { export function turnScreen(pack, state, schema, view, actions) {
const heading = turnHeading(pack, state); const heading = turnHeading(pack, state);
@@ -35,12 +33,12 @@ export function turnScreen(pack, state, schema, view, actions) {
h('div', { class: 'layout' }, h('div', { class: 'layout' },
h('main', { class: 'scene' }, h('main', { class: 'scene' },
view.phase === 'outcome' view.phase === 'outcome'
? outcomeView(pack, state, view, actions) ? outcomeView(pack, view, actions)
: choosingView(pack, state, view, actions), : choosingView(pack, view, actions),
), ),
h('aside', { class: 'people' }, h('aside', { class: 'people' },
h('h2', { class: 'people__heading' }, pack.peopleHeading ?? 'The people'), h('h2', { class: 'people__heading' }, pack.peopleHeading ?? 'The people'),
presentPeople(pack, state).map((person) => characterCard(pack, state, schema, person)), (pack.characters ?? []).map((character) => characterCard(pack, state, schema, character)),
), ),
), ),
@@ -86,55 +84,44 @@ function diary(pack, state) {
); );
} }
function choosingView(pack, state, view, actions) { function choosingView(pack, view, actions) {
const { event, options } = view.turn; const { event, options } = view.turn;
const text = (t) => fillText(pack, state, t, view.turn.bindings);
const visible = options.filter((o) => !o.hidden); const visible = options.filter((o) => !o.hidden);
// The first option a player can actually take gets the focus, so Enter plays // The first option a player can actually take gets the focus, so Enter plays
// it and Tab walks to the others. // it and Tab walks to the others.
const firstAvailable = visible.find((o) => o.available)?.option.id; const firstAvailable = visible.find((o) => o.available)?.option.id;
return h('section', { class: 'scenario' }, return h('section', { class: 'scenario' },
h('p', { class: 'scenario__text' }, text(event.description)), h('p', { class: 'scenario__text' }, event.description),
h('ul', { class: 'options' }, h('ul', { class: 'options' },
visible.map(({ option, available, failed, odds }) => h('li', { class: 'options__item' }, visible.map(({ option, available, failed }) => h('li', { class: 'options__item' },
h('button', { h('button', {
class: `option${available ? '' : ' option--locked'}`, class: `option${available ? '' : ' option--locked'}`,
disabled: !available, disabled: !available,
dataset: option.id === firstAvailable ? { autofocus: 'true' } : {}, dataset: option.id === firstAvailable ? { autofocus: 'true' } : {},
onClick: available ? () => actions.onChoose(option.id) : null, onClick: available ? () => actions.onChoose(option.id) : null,
}, },
h('span', { class: 'option__label' }, text(option.label)), h('span', { class: 'option__label' }, option.label),
// Odds are shown in words, never as a number: "a long shot", "likely". !available && h('span', { class: 'option__requirement' }, requirementText(pack, failed)),
available && odds && h('span', { class: 'option__odds' }, odds.words),
!available && h('span', { class: 'option__requirement' }, requirementText(pack, failed, state)),
), ),
)), )),
), ),
); );
} }
function outcomeView(pack, state, view, actions) { function outcomeView(pack, view, actions) {
const { record, option } = view; const { record, option } = view;
// The state has already moved on to the next event, so the names in this
// outcome come from the bindings the turn was played with, not the new ones.
const text = (t) => fillText(pack, state, t, record.bindings ?? {});
const branch = record.check ? option[record.check.outcome] : null;
const result = branch ? branch.result : option.result;
const said = pack.checks?.said ?? { success: 'It came off.', failure: 'It did not come off.' };
return h('section', { class: 'outcome' }, return h('section', { class: 'outcome' },
h('p', { class: 'outcome__chose' }, record.optionLabel ?? text(option.label)), h('p', { class: 'outcome__chose' }, option.label),
record.check && h('p', { class: `outcome__check outcome__check--${record.check.outcome}` }, option.result && h('p', { class: 'outcome__text' }, option.result),
`${capitalise(record.check.odds)} — ${said[record.check.outcome]}`),
result && h('p', { class: 'outcome__text' }, text(result)),
h('div', { class: 'outcome__ledger' }, h('div', { class: 'outcome__ledger' },
h('h3', { class: 'outcome__heading' }, 'What that changed'), h('h3', { class: 'outcome__heading' }, 'What that changed'),
changeList(pack, record.changes, { state }), changeList(pack, record.changes),
record.upkeep.length > 0 && h('div', { class: 'outcome__upkeep' }, record.upkeep.length > 0 && h('div', { class: 'outcome__upkeep' },
h('h3', { class: 'outcome__heading' }, 'And then the day happened'), h('h3', { class: 'outcome__heading' }, 'And then the day happened'),
changeList(pack, record.upkeep, { state }), changeList(pack, record.upkeep),
), ),
), ),
@@ -198,5 +185,3 @@ function feedbackWidget(view, actions) {
feedback.saved && h('p', { class: 'feedback__saved' }, 'Noted — thank you.'), feedback.saved && h('p', { class: 'feedback__saved' }, 'Noted — thank you.'),
); );
} }
const capitalise = (words) => words.charAt(0).toUpperCase() + words.slice(1);
-7
View File
@@ -171,13 +171,6 @@ body {
.option__requirement { .option__requirement {
font-size: 12px; letter-spacing: 0.04em; color: var(--ink-faint); font-style: italic; font-size: 12px; letter-spacing: 0.04em; color: var(--ink-faint); font-style: italic;
} }
/* The odds, in words: "a long shot", "likely". Never a number. */
.option__odds {
font-size: 12px; letter-spacing: 0.04em; color: var(--accent); font-style: italic;
}
.outcome__check { margin: 0 0 8px; font-size: 14px; font-style: italic; }
.outcome__check--success { color: var(--up); }
.outcome__check--failure { color: var(--down); }
/* ---------- outcome ---------- */ /* ---------- outcome ---------- */
+1 -1
View File
@@ -8,7 +8,7 @@ export const APP = {
// Kept in step with package.json by a test, because it silently was not: the // Kept in step with package.json by a test, because it silently was not: the
// status bar read 0.3.0 through the 0.3.1 and 0.3.2 work, which is exactly // status bar read 0.3.0 through the 0.3.1 and 0.3.2 work, which is exactly
// the thing a playtester is told to check to know what they are playing. // the thing a playtester is told to check to know what they are playing.
version: '0.4.0', version: '0.3.3',
}; };
/** "0.3.0 · a1b2c3d" — or "0.3.0 · dev" when running from source unstamped. */ /** "0.3.0 · a1b2c3d" — or "0.3.0 · dev" when running from source unstamped. */
-185
View File
@@ -1,185 +0,0 @@
// The cast drawn per run, names in text, and options whose outcome is a check.
// See DECISIONS #41. Runs against test/fixtures/cast-pack.js, never real content.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { startGame, currentTurn, takeTurn } from '../src/engine/game.js';
import { validatePack } from '../src/engine/validate.js';
import { hydrate, createState, resolveSchema } from '../src/engine/state.js';
import { presentPeople, presentSlots, personIn } from '../src/engine/cast.js';
import { fillText } from '../src/engine/text.js';
import { chanceOf, oddsWords } from '../src/engine/checks.js';
import { getPath, setPath } from '../src/engine/paths.js';
import { castPack } from './fixtures/cast-pack.js';
import { testPack } from './fixtures/test-pack.js';
const clone = (value) => structuredClone(value);
/** Advance until the current event is `eventId`, taking `work` otherwise. */
function until(state, eventId, pack = castPack) {
let next = state;
for (let i = 0; i < 200 && currentTurn(pack, next).event.id !== eventId; i++) {
next = takeTurn(pack, next, currentTurn(pack, next).options[0].option.id).state;
}
assert.equal(currentTurn(pack, next).event.id, eventId);
return next;
}
test('the cast pack is valid, with no warnings', () => {
const { errors, warnings } = validatePack(castPack);
assert.deepEqual(errors, []);
assert.deepEqual(warnings, []);
});
test('a run draws one boss and two to four different co-workers', () => {
const counts = new Set();
for (let seed = 0; seed < 40; seed++) {
const state = startGame(castPack, { seed });
const people = presentPeople(castPack, state);
assert.equal(people.filter((p) => p.role === 'boss').length, 1);
const coworkers = presentSlots(castPack, state, 'coworker');
assert.ok(coworkers.length >= 2 && coworkers.length <= 4, `drew ${coworkers.length}`);
counts.add(coworkers.length);
const names = people.map((p) => p.name);
assert.equal(new Set(names).size, names.length, 'nobody is drawn twice');
assert.ok(names.includes('Old Sam'), 'a fixed character is always present');
}
assert.deepEqual([...counts].sort(), [2, 3, 4], 'every head count turns up across seeds');
});
test('the cast is the seed\'s: the same seed draws the same people, others differ', () => {
const names = (seed) => presentPeople(castPack, startGame(castPack, { seed })).map((p) => p.name).join('|');
assert.equal(names('a'), names('a'));
const distinct = new Set(Array.from({ length: 20 }, (_, i) => names(`s${i}`)));
assert.ok(distinct.size > 10, `only ${distinct.size} different casts in 20 runs`);
});
test('an empty group slot is not in the run, and a drawn person brings their own starting regard', () => {
let seed = 0;
let state;
do state = startGame(castPack, { seed: seed++ });
while (presentSlots(castPack, state, 'coworker').length !== 2 || personIn(castPack, state, 'boss').name !== 'Walter Grose');
assert.equal(getPath(state, 'cast.coworker_3'), null);
assert.equal(personIn(castPack, state, 'coworker_3'), undefined);
assert.equal(getPath(state, 'relationships.boss.trust'), 30);
});
test('names are filled into text from whoever was drawn', () => {
const state = startGame(castPack, { seed: 'names' });
const boss = personIn(castPack, state, 'boss');
assert.equal(fillText(castPack, state, 'Ask {boss.first}, the {boss.title}.'),
`Ask ${boss.name.split(' ')[0]}, the Manager.`);
assert.equal(fillText(castPack, state, '{porter} nods.'), 'Old Sam nods.');
assert.equal(fillText(castPack, state, '{nobody} stays as written.'), '{nobody} stays as written.');
});
test('an event about a co-worker binds one who is present, and its effects land on them', () => {
const state = until(startGame(castPack, { seed: 'bind' }), 'needled');
const turn = currentTurn(castPack, state);
const slot = turn.bindings.coworker;
assert.ok(presentSlots(castPack, state, 'coworker').includes(slot));
assert.match(fillText(castPack, state, turn.event.description), new RegExp(personIn(castPack, state, slot).name));
const before = getPath(state, `relationships.${slot}.regard`);
const { state: after, record } = takeTurn(castPack, state, 'ignore');
assert.equal(getPath(after, `relationships.${slot}.regard`), before - 2);
assert.deepEqual(record.bindings, { coworker: slot });
});
test('the chance follows the formula, and the odds are words', () => {
let state = startGame(castPack, { seed: 'odds' });
const option = castPack.events[0].options[2]; // wits against difficulty 5
state = setPath(state, 'attributes.wits', 5);
assert.equal(chanceOf(castPack, state, option).chance, 50);
state = setPath(state, 'attributes.wits', 7);
assert.equal(chanceOf(castPack, state, option).chance, 70);
state = setPath(state, 'attributes.wits', 10);
assert.equal(chanceOf(castPack, state, option).chance, 100, 'clamped at the ceiling');
assert.equal(oddsWords(castPack, 15), 'a long shot');
assert.equal(oddsWords(castPack, 50), 'an even chance');
assert.equal(oddsWords(castPack, 90), 'a safe bet');
});
test('an opposed check reads the other person\'s attribute, and their regard moves it', () => {
let state = startGame(castPack, { seed: 'opposed' });
const option = castPack.events[0].options[1]; // charm against the boss's wits, leaning on trust
const boss = personIn(castPack, state, 'boss');
state = setPath(state, 'attributes.charm', 6);
state = setPath(state, 'relationships.boss.trust', 50);
const even = chanceOf(castPack, state, option);
assert.equal(even.theirs, boss.attributes.wits);
assert.equal(even.chance, Math.min(100, Math.max(0, 50 + 10 * (6 - boss.attributes.wits))));
state = setPath(state, 'relationships.boss.trust', 70);
assert.equal(chanceOf(castPack, state, option).chance, Math.min(100, even.chance + 10));
});
test('a check applies the branch it rolled, plus the option\'s own effects', () => {
let state = until(startGame(castPack, { seed: 'roll' }), 'routine');
const sure = setPath(setPath(state, 'attributes.charm', 10), 'relationships.boss.trust', 100);
const won = takeTurn(castPack, sure, 'ask_for_more');
assert.equal(won.record.check.outcome, 'success');
assert.equal(getPath(won.state, 'money'), getPath(sure, 'money') + 50);
assert.equal(getPath(won.state, 'relationships.boss.regard'), getPath(sure, 'relationships.boss.regard') - 1);
const hopeless = setPath(setPath(state, 'attributes.charm', 0), 'relationships.boss.trust', 0);
const lost = takeTurn(castPack, hopeless, 'ask_for_more');
assert.equal(lost.record.check.outcome, 'failure');
assert.equal(getPath(lost.state, 'money'), getPath(hopeless, 'money'));
assert.equal(getPath(lost.state, 'relationships.boss.trust'), 0, 'clamped');
assert.match(lost.record.check.odds, /long shot/);
});
test('a run with checks and bindings replays identically from its seed and choices', () => {
const play = () => {
let state = startGame(castPack, { seed: 'replay' });
const records = [];
for (let i = 0; i < 60; i++) {
const turn = currentTurn(castPack, state);
const pick = turn.options[i % turn.options.length].option.id;
const result = takeTurn(castPack, state, pick);
records.push(result.record);
state = i === 30 ? JSON.parse(JSON.stringify(result.state)) : result.state;
}
return { state, records };
};
const a = play();
const b = play();
assert.deepEqual(a.records.map((r) => r.check?.outcome ?? '-'), b.records.map((r) => r.check?.outcome ?? '-'));
assert.deepEqual(a.state, b.state);
assert.ok(a.records.some((r) => r.check?.outcome === 'success') && a.records.some((r) => r.check?.outcome === 'failure'));
});
test('a save from before the pack drew a cast gets one on load, the same one every time', () => {
const schema = resolveSchema(castPack);
const fresh = createState(castPack, { seed: 'old', schema });
const old = clone(fresh);
delete old.cast;
const once = hydrate(castPack, clone(old), schema);
const twice = hydrate(castPack, clone(old), schema);
assert.ok(once.added.includes('cast'));
assert.deepEqual(once.state.cast, twice.state.cast);
assert.deepEqual(once.state.cast, fresh.cast);
});
test('a pack with no drawn cast gets no cast and no bindings: its runs are unchanged', () => {
const state = startGame(testPack, { seed: 'plain' });
assert.equal(state.cast, undefined);
assert.equal(state.meta.bindings, undefined);
assert.equal(presentPeople(testPack, state).map((p) => p.name).join(), 'The Chief,Pat');
});
test('the validator catches cast and check mistakes', () => {
const broken = clone(castPack);
broken.events[0].options[1].check.attribute = 'strength';
broken.events[0].options[2].failure = undefined;
broken.events[0].description = 'Blame {coworker}.';
broken.events.push({ id: 'ghost', stage: 'desk', description: '{ghost} appears.', options: [{ id: 'x', label: 'x', effects: [] }] });
broken.characters[1].count = [2, 6];
const { errors } = validatePack(broken);
const has = (fragment) => assert.ok(errors.some((e) => e.includes(fragment)), `expected an error mentioning "${fragment}":\n${errors.join('\n')}`);
has('attribute "strength" is not in pack.attributes');
has('has no failure');
has('{coworker} is a group');
has('{ghost} names no character');
has('has only 5 candidates');
});
-35
View File
@@ -132,41 +132,6 @@ for (const pack of packs) {
assert.deepEqual(trapped, [], 'events whose every option is gated'); assert.deepEqual(trapped, [], 'events whose every option is gated');
}); });
test(`${where} no repayment can take more than is owed`, () => {
// A fixed instalment against a smaller balance takes money for a debt that
// does not exist; the Frontier's store did it to a real player. A balance
// is any money-formatted stat other than money itself. An option that pays
// one down with money must read the amount from the balance, or be gated
// on the balance covering the whole instalment. See DECISIONS #38.
const balances = Object.entries(pack.display ?? {})
.filter(([path, rule]) => rule.format === 'money' && path !== 'money')
.map(([path]) => path);
const overcharging = [];
for (const event of pack.events) {
for (const option of event.options ?? []) {
// A check's branches pay and spend too (DECISIONS #41): each is judged
// together with the effects that apply either way.
const branches = option.check
? [option.success, option.failure].map((b) => [...(option.effects ?? []), ...(b?.effects ?? [])])
: [option.effects ?? []];
for (const effects of branches) {
const spends = effects.some((e) => e.path === 'money' && e.op === 'add'
&& (typeof e.value === 'number' ? e.value < 0 : (e.value?.times ?? 1) < 0));
if (!spends) continue;
for (const e of effects) {
if (!balances.includes(e.path) || e.op !== 'add') continue;
if (typeof e.value !== 'number' || e.value >= 0) continue;
const covered = (option.requires ?? [])
.some((r) => r.path === e.path && r.op === '>=' && r.value >= -e.value);
if (!covered) overcharging.push(`${event.id}.${option.id} (${e.path} ${e.value})`);
}
}
}
}
assert.ok(balances.length > 0 || pack.display === undefined, 'a pack with a balance should declare it as money');
assert.deepEqual(overcharging, [], 'options that can charge for more than is owed');
});
test(`${where} every turn always offers something to do, however badly it is going`, () => { test(`${where} every turn always offers something to do, however badly it is going`, () => {
for (let seed = 100; seed < 130; seed++) randomRun(pack, seed, 150); for (let seed = 100; seed < 130; seed++) randomRun(pack, seed, 150);
}); });
-31
View File
@@ -67,37 +67,6 @@ test('an unknown operation is a loud failure', () => {
assert.throws(() => applyEffect(state, { path: 'money', op: 'multiply', value: 2 }), /unknown effect operation/); assert.throws(() => applyEffect(state, { path: 'money', op: 'multiply', value: 2 }), /unknown effect operation/);
}); });
test('an amount can be read from state, capped and signed', () => {
// The Frontier's store, turn 27 of a real session: four owed, twenty taken.
const ledger = { owed: { initial: 0, min: 0, max: 1000 } };
const pay = [
{ path: 'money', op: 'add', value: { from: 'owed', times: -1, cap: 20 } },
{ path: 'owed', op: 'add', value: { from: 'owed', times: -1, cap: 20 } },
];
const small = applyEffects({ money: 110, owed: 4 }, pay, ledger);
assert.equal(small.state.money, 106, 'pays what is owed, not the instalment');
assert.equal(small.state.owed, 0);
assert.equal(small.changes[1].clamped, false, 'nothing is lost to a clamp');
assert.equal(small.changes[0].value, -4, 'the change records the amount applied');
assert.deepEqual(small.changes[0].source, { from: 'owed', times: -1, cap: 20 });
const large = applyEffects({ money: 110, owed: 50 }, pay, ledger);
assert.equal(large.state.money, 90, 'the cap holds when more is owed');
assert.equal(large.state.owed, 30);
});
test('an amount read from a path that is not a number is zero', () => {
const result = applyEffect({ money: 10 }, { path: 'money', op: 'add', value: { from: 'owed', times: -1 } }, {});
assert.equal(result.state.money, 10);
assert.equal(result.change.value === 0 || Object.is(result.change.value, -0), true);
});
test('a literal amount records no source', () => {
const result = applyEffect(state, { path: 'money', op: 'add', value: 5 }, schema);
assert.equal('source' in result.change, false);
});
test('an empty effect list is a no-op', () => { test('an empty effect list is a no-op', () => {
const result = applyEffects(state, [], schema); const result = applyEffects(state, [], schema);
assert.deepEqual(result.changes, []); assert.deepEqual(result.changes, []);
-114
View File
@@ -1,114 +0,0 @@
// 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;
+7 -35
View File
@@ -13,41 +13,13 @@
import { startGame, currentTurn, takeTurn } from '../../src/engine/game.js'; import { startGame, currentTurn, takeTurn } from '../../src/engine/game.js';
import { createRng } from '../../src/engine/rng.js'; import { createRng } from '../../src/engine/rng.js';
/** const moneyGain = (resolved) => (resolved.option.effects ?? [])
* What an effect is worth to a bot. An amount read from state (DECISIONS #38) .filter((e) => e.path === 'money' && e.op === 'add')
* is scored at its cap: the most it could be, which is what a player weighing .reduce((sum, e) => sum + e.value, 0);
* the option sees on the label.
*/
const amountOf = (effect) => {
if (typeof effect.value === 'number') return effect.value;
if (typeof effect.value?.cap === 'number') return effect.value.cap * (effect.value.times ?? 1);
return 0;
};
/** const relationshipGain = (resolved) => (resolved.option.effects ?? [])
* Every effect an option can have, with the weight a player reading the odds .filter((e) => e.path.startsWith('relationships.'))
* would give it: plain effects count in full, a check's success and failure by .reduce((sum, e) => sum + e.value, 0);
* 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 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]; const best = (options, score) => [...options].sort((a, b) => score(b) - score(a))[0];
@@ -76,7 +48,7 @@ export const strategies = {
*/ */
lifer: (options) => { lifer: (options) => {
const stays = options.filter( const stays = options.filter(
(o) => !weightedEffects(o).some(({ effect }) => effect.path === 'stage'), (o) => !(o.option.effects ?? []).some((e) => e.path === 'stage'),
); );
return best(stays.length ? stays : options, relationshipGain); return best(stays.length ? stays : options, relationshipGain);
}, },
+1 -31
View File
@@ -11,8 +11,6 @@ import assert from 'node:assert/strict';
import { frontier as pack } from '../content/frontier/index.js'; import { frontier as pack } from '../content/frontier/index.js';
import { startGame, currentTurn, takeTurn } from '../src/engine/game.js'; import { startGame, currentTurn, takeTurn } from '../src/engine/game.js';
import { check } from '../src/engine/conditions.js'; import { check } from '../src/engine/conditions.js';
import { applyEffects } from '../src/engine/effects.js';
import { resolveSchema } from '../src/engine/state.js';
import { play, sweep, median } from './fixtures/strategies.js'; import { play, sweep, median } from './fixtures/strategies.js';
/** /**
@@ -60,7 +58,7 @@ test('the settlement and the room land on the days the pack says', () => {
const turn = currentTurn(pack, state); const turn = currentTurn(pack, state);
const result = takeTurn(pack, state, turn.options.find((o) => o.available).option.id); const result = takeTurn(pack, state, turn.options.find((o) => o.available).option.id);
for (const change of result.record.upkeep) { for (const change of result.record.upkeep) {
if (change.label === 'Wages from Vane') settlement.push(state.turn); if (change.label === 'Settlement') settlement.push(state.turn);
if (change.label === 'Room at the Cordwood') room.push(state.turn); if (change.label === 'Room at the Cordwood') room.push(state.turn);
} }
state = result.state; state = result.state;
@@ -252,34 +250,6 @@ test('a player who wants a skill does not wait long for the first door', () => {
} }
}); });
test('the horse and the room are each offered only while they can still be bought', () => {
// A real session was shown "Buy the bay" locked after buying it, and offered
// the room under a description about the horse. See DECISIONS #40.
const offered = (lifestyle) => pack.events
.filter((e) => ['the_sale_barn', 'the_ellis_room'].includes(e.id))
.filter((e) => check({ stage: 'deputy', lifestyle }, e.requires).passed)
.map((e) => e.id);
assert.deepEqual(offered(0), ['the_sale_barn']);
assert.deepEqual(offered(1), ['the_ellis_room']);
assert.deepEqual(offered(2), []);
});
test('paying the book down never takes more than is owed', () => {
// Turn 27 of that session: four dollars owed, twenty taken. See DECISIONS #38.
const schema = resolveSchema(pack);
const payDown = pack.events.find((e) => e.id === 'the_store')
.options.find((o) => o.id === 'pay_down');
const base = startGame(pack, { seed: 'the-book' });
const small = applyEffects({ ...base, money: 110, owed: 4 }, payDown.effects, schema).state;
assert.equal(small.money, 106, 'four owed costs four');
assert.equal(small.owed, 0);
const large = applyEffects({ ...base, money: 110, owed: 50 }, payDown.effects, schema).state;
assert.equal(large.money, 90, 'more than twenty owed costs twenty');
assert.equal(large.owed, 30);
});
test('a deliberate player can put Dutch in their debt', () => { test('a deliberate player can put Dutch in their debt', () => {
// The deepest relationship gate in the pack: `send_for_dutch` in the // The deepest relationship gate in the pack: `send_for_dutch` in the
// marshal's office needs a favour banked two rungs earlier. // marshal's office needs a favour banked two rungs earlier.
-1
View File
@@ -141,7 +141,6 @@ test('the turn record carries the diff and the options the player could not take
const locked = record.offered.find((o) => o.id === 'charm'); const locked = record.offered.find((o) => o.id === 'charm');
assert.equal(locked.available, false); assert.equal(locked.available, false);
assert.equal(locked.failed[0].path, 'skills.talking'); assert.equal(locked.failed[0].path, 'skills.talking');
assert.equal(locked.hidden, false, 'the record says whether a locked option was on screen');
}); });
test('history accumulates every turn and is what the retrospective reads', () => { test('history accumulates every turn and is what the retrospective reads', () => {
-49
View File
@@ -527,52 +527,3 @@ test('the header reads the second pack\'s display map, not the first\'s', async
dom.restore(); dom.restore();
} }
}); });
// ---- a drawn cast, and checks (DECISIONS #41) ----------------------------------
test('a pack with a drawn cast shows whoever this run drew, and never a placeholder', async () => {
const dom = await boot({ pack: 'countinghouse' });
try {
const people = byClass(dom.root, 'person');
// One partner, one chief clerk, and two to four clerks.
assert.ok(people.length >= 4 && people.length <= 6, `${people.length} people in the panel`);
const titles = byClass(dom.root, 'person__title').map((t) => t.textContent);
assert.equal(titles.filter((t) => t === 'Senior Partner').length, 1);
assert.equal(titles.filter((t) => t === 'Chief Clerk').length, 1);
const clerks = titles.filter((t) => t === 'Clerk').length;
assert.ok(clerks >= 2 && clerks <= 4, `${clerks} clerks`);
for (let turn = 0; turn < 25; turn++) {
assert.doesNotMatch(text(dom.root), /\{[a-z_0-9.]+\}/, 'a {placeholder} reached the screen');
enabledOptions(dom.root)[0].click();
assert.doesNotMatch(text(dom.root), /\{[a-z_0-9.]+\}/, 'a {placeholder} reached the outcome');
firstByClass(dom.root, 'button--next').click();
}
} finally {
dom.restore();
}
});
test('a checked option shows its odds in words, and the outcome says whether it came off', async () => {
const dom = await boot({ pack: 'countinghouse' });
try {
let checked;
for (let turn = 0; turn < 40 && !checked; turn++) {
checked = optionButtons(dom.root).find((b) => !b.disabled && firstByClass(b, 'option__odds'));
if (checked) break;
enabledOptions(dom.root)[0].click();
firstByClass(dom.root, 'button--next').click();
}
assert.ok(checked, 'a checked option turned up within forty days');
const odds = firstByClass(checked, 'option__odds').textContent;
assert.match(odds, /^(a long shot|unlikely|an even chance|likely|a safe bet)$/);
assert.doesNotMatch(text(dom.root), /\d+%/, 'no percentage is ever shown');
checked.click();
const said = text(firstByClass(dom.root, 'outcome__check'));
assert.match(said, /and it (came|did not come) off\./);
assert.ok(said.toLowerCase().startsWith(odds), `the outcome repeats the odds: "${said}"`);
assert.ok(firstByClass(dom.root, 'outcome__text'), 'and says what happened');
} finally {
dom.restore();
}
});
-17
View File
@@ -25,23 +25,6 @@ test('an effect writing an undeclared path is caught', () => {
assert.match(messages(result), /skills\.wleding.*not declared/s); assert.match(messages(result), /skills\.wleding.*not declared/s);
}); });
test('an amount read from state must read a declared path, with numeric scale and cap', () => {
const unknown = broken((pack) => {
pack.events[0].options[0].effects.push({ path: 'money', op: 'add', value: { from: 'skills.wleding', times: -1 } });
});
assert.match(messages(unknown), /reads its amount from "skills\.wleding".*not declared/s);
const badCap = broken((pack) => {
pack.events[0].options[0].effects.push({ path: 'money', op: 'add', value: { from: 'money', cap: 'lots' } });
});
assert.match(messages(badCap), /non-numeric cap/);
const fine = broken((pack) => {
pack.events[0].options[0].effects.push({ path: 'money', op: 'add', value: { from: 'money', times: -1, cap: 5 } });
});
assert.deepEqual(fine.errors, []);
});
test('a relationship effect naming a character who does not exist is caught', () => { test('a relationship effect naming a character who does not exist is caught', () => {
const result = broken((pack) => { const result = broken((pack) => {
pack.events[0].options[0].effects.push({ path: 'relationships.dave.likes_you', op: 'add', value: 1 }); pack.events[0].options[0].effects.push({ path: 'relationships.dave.likes_you', op: 'add', value: 1 });
+3 -13
View File
@@ -17,18 +17,11 @@ import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const logsDir = resolve(root, 'logs'); const logsDir = resolve(root, 'logs');
/** /** The most recently modified log in logs/, when none was named. */
* The most recently modified feedback log in logs/, when none was named.
*
* Feedback logs only. A save export is a .json too, and a playtester who
* exports both — as the first one to do so did — would otherwise have the save
* read as a log whenever it was the later of the two.
*/
async function newestLog() { async function newestLog() {
let names; let names;
try { try {
names = (await readdir(logsDir)) names = (await readdir(logsDir)).filter((name) => name.endsWith('.json'));
.filter((name) => name.startsWith('theladder-feedback-') && name.endsWith('.json'));
} catch { } catch {
return null; return null;
} }
@@ -137,10 +130,7 @@ function reportGates(entries) {
const blocked = new Map(); const blocked = new Map();
for (const entry of entries) { for (const entry of entries) {
for (const option of entry.offered ?? []) { for (const option of entry.offered ?? []) {
// A hidden option was never on screen, so it was never a door anybody if (option.available) continue;
// met. Logs from before 0.3.4 do not record `hidden`, and still count
// such options here. See DECISIONS #39.
if (option.available || option.hidden) continue;
const reasons = (option.failed ?? []) const reasons = (option.failed ?? [])
.map((c) => `${c.path} ${c.op} ${JSON.stringify(c.value)}`) .map((c) => `${c.path} ${c.op} ${JSON.stringify(c.value)}`)
.join(', ') || 'unstated'; .join(', ') || 'unstated';
+14
View File
@@ -0,0 +1,14 @@
# story-to-pack lives on its own branch and is never imported by the game.
# The repo root ignores *.log. Here the run logs are the record of results, so keep them.
!*.log
# Inference-host telemetry: large, and it names the host.
probe/gpu-logs/
# Rebuildable caches — gate2.py, gate3.py and signals.py recreate them on first use.
probe/sim.pkl
probe/null.pkl
probe/null-samples.pkl
probe/coassoc.json
__pycache__/
+275
View File
@@ -0,0 +1,275 @@
# story-to-pack — design note
A tool that builds a playable The Ladder content pack from a corpus of stories.
**Status (2026-09-14): direction agreed, nothing built.** The decisions below were made in
conversation on 2026-09-10 and had until now existed only in that session's transcript. The
architecture rests on one assumption that has so far **failed** its first test (see
[Assumptions](#assumptions-under-test)); nothing past the probes should be built until it passes.
This is a design note, not a specification. It records what was decided and why, so that the next
step is argued from it rather than re-derived.
---
## What it is for
Feed it a body of fiction set in one world — say 20–30 short stories — plus a handful of structural
numbers, and get back a content pack the game can load and that its own conformance checks judge
viable. Then play it, hand-edit it, and retune it from play logs.
The Ladder's founding commitment (Planning/01 §9) is that a new setting is purely data. This tool
tests how far that goes: whether "data" can come from a corpus rather than a designer.
## Decisions
| Area | Decision |
| --- | --- |
| **Inputs** | A thematically coherent corpus (one world), 20–30 stories of 2–15 pages. Plus: number of stages, turns per stage, number of promotions, number of characters, and a one-line player role written by the designer. Early development uses much less text. |
| **Corpus** | Saved permanently beside the pack. Read once, at fixed depth. **Provenance recorded:** which chunks fed which event, even though nothing reads it yet. |
| **Artifact** | **One.** The generated pack is the source of truth from the moment it is written. The designer and the tool both edit that same file in place. Seed text is a seed, not a source: there is no regenerate-and-merge. |
| **Pipeline** | Chunk → embed → cluster → select recurring situations → extract prose → assign state paths and directions → search numbers → verify → repair. |
| **The model's job** | Selection and classification. **It never chooses a number.** Its generative tasks are entity substitution and, only where needed, filling out options. |
| **Inference** | Once, up front, checkpointed and cached. The numeric search and every repair iteration are pure computation and never call a model. This is what makes a small local model viable. |
| **Stat vocabulary** | A fixed mechanical spine (currency, debt, standing, two skills, flags, notable, per-character relationship stats). The corpus supplies the *labels*, not the structure. |
| **Cast** | Recurring **roles** harvested across the corpus (the authority, the rival, the peer), not people from any one story. Characters are minted for the roles and entity names are substituted in extracted prose. |
| **Stages** | Assigned automatically by the protagonist's standing in each scene, binned and balanced across stages. Overridable by hand. |
| **Event count** | Derived from turns per stage, with a sufficiency report *before* inference is spent: "this corpus yields N situations, the pack needs M". |
| **Options** | 3–5 per event: 1–3 fixed archetypes ("say nothing"), 1–3 drawn from other scenes in the cluster, and 1–3 generated only to fill gaps. Deduplicated **mechanically** — two options with the same effects are one option. |
| **Description variants** | An event can carry several descriptions, sharing one general option set. This needs an engine change (see Open questions). |
| **Viability** | Fully automatic — a human does not have to play a pack for it to count as viable. A **graded score**, not pass/fail, so extra time improves it. The search **always keeps the best-scoring candidate**, never the most recent. When it cannot reach viable, it reports precisely which checks it could not satisfy. |
| **Time and quality** | Start fast and iterate; later, overnight runs for higher quality. Time buys **numeric search depth and prose fidelity** — not corpus reach. |
| **Retuning** | Deterministic, driven by the play log `npm run analyze` already reads. Not model-mediated. |
| **Extension** | Pulling more corpus into an existing pack is **out of scope** for now. Provenance is recorded so it stays possible. |
| **Home** | `theladder/tools/`, never imported by `src/`, `content/` or `test/`. Its own dependencies stay out of the game. |
| **Hardware** | Must be able to run on a low-powered local inference host, taking longer if it has to. |
## Why the hard part is numbers, not prose
Counted from the two hand-written packs:
| | Corporate Ladder | The Frontier |
| --- | --- | --- |
| Prose strings | 219 | 211 |
| Numeric decisions | 347 | 427 |
| Stable ids | 125 | 120 |
A pack is roughly 35% prose and 65% numbers, and every defect the project has hit — the debt trap,
the rent retune, the negotiation cliff — lived in the numbers. The corpus supplies the prose; the
tool has to invent the rest, and has to be able to show it is not a trap.
Randomising The Frontier's numbers while keeping its prose and structure: random magnitudes pass the
conformance checks 3% of the time, random directions 0%, and a crude four-rule repair loop 42% (5 of
12 runs converged). The search space is not hopeless, but it is not a broad basin either — hence a
real search against a graded score.
## Why recurrence is the central problem
Hand-written packs are almost entirely **recurring** situations: 29 of 31 events in Corporate Ladder
and 28 of 29 in The Frontier can fire more than once, and a 41-turn playtest saw 12 distinct
situations an average of 3.4 times each. Stories are the opposite: every scene happens once.
So selection is not "pick 30 good scenes". It is **find the situations this corpus keeps returning
to**. Twenty stories in one world should contain eight variations on "a superior asks you to cover
something up"; clustering those is what yields one recurring event with several descriptions. One
long novel is the hard case and is not the target.
## Boundaries
- **Planning/01 §10 is not violated by a hosted model here.** §10 says the *game* must never require
a hosted inference service. The generator is a build-time tool whose output is plain data with no
runtime dependency; a shipped `.html` still plays offline. Record this in theladder's DECISIONS
when the tool lands there.
- **There are two provider slots, not one.** Embeddings and generation are configured separately.
Anthropic offers generation but no embeddings API, so any mix must keep embeddings local.
- **The inference host is shared.** Other work sends requests to the same server. Clients run one
request at a time with a pause between requests, set a short `keep_alive`, and read the host from
the environment (`STP_OLLAMA`) — never from a tracked file. Every run is agreed with its owner
before it starts.
## Assumptions under test
The architecture rests on three assumptions. All three are cheap to test, and the first could
invalidate everything else.
### 1. A coherent corpus clusters into recurring situations — FAILED in naive form
**Test (2026-09-10):** O. Henry's *Four Million* cycle — four Gutenberg volumes, 94 stories, 244,690
words — cut into 838 scenes of ~300 words, embedded with `nomic-embed-text`, clustered with k-means.
**Result:** at the best k, 18 clusters were both tight and drawn from four or more stories. But
reading them showed most were 60–79% **one** story plus stragglers. Measured by concentration, only
**3 of 18** were genuinely recurring, covering **2% of the corpus**. At scene length, embedding
similarity tracks *which story a scene is from* — names, setting, narrator voice — not *what kind of
situation it is*.
**Fix under test:** summarise each scene into one generic sentence with no proper names, embed the
summary, and cluster those instead. This probe is in `probe/`.
**Baseline re-measured (2026-09-14)** on the rebuilt corpus — prose embeddings made on a GPU host,
same model, same 838 chunks. Seed 11 at k = 100 reproduces the 2026-09-10 figures exactly (18 sharp
clusters spanning ≥ 4 stories, 3 genuinely recurring, 2% coverage). Seeds 12 and 13 give 7 and 7,
covering 8% and 10%: the old 2% was one unlucky initialisation, and the true baseline is poor rather
than hopeless.
**Pass mark, fixed before any summary existed.** Judged at **k = 60 and k = 100**, medians over
three seeds, summary embeddings against prose embeddings as reported by `probe/measure.py`:
1. Median dominant-story share of clusters of size ≥ 5 falls to **40% or below** at both k.
2. **Tight-recurring coverage** — recurring clusters (≥ 4 stories, dominant ≤ 40%) whose coherence is
at or above that file's median cluster — reaches **at least 10% of the corpus** at both k. (A
relative bar was written first and dropped once the baseline came in at 2% and 0%, where "double"
means nothing. 10% of 838 scenes is ~84 — enough raw material for a stage's worth of situations.)
3. Reading three sampled tight-recurring clusters, each is recognisably **one kind of situation**,
not a prose register ("men walking through the city") or a vocabulary cluster.
The prose baseline numbers the first two are judged against are in `probe/measure-prose.log`.
**Result (2026-09-14): criteria 1 and 2 pass decisively; criterion 3 passes only in part.**
| k | Median dominant share, prose → summaries | Tight-recurring coverage, prose → summaries |
| --- | --- | --- |
| 60 | 57% → **22%** | 2% → **26%** |
| 100 | 67% → **28%** | 0% → **19%** |
Pipeline as run: `qwen2.5:3b-instruct` summaries with the v2 prompt (no example roles), names removed
by `strip_names.py`, embedded with `nomic-embed-text`. Logs: `probe/measure-summary.log`,
`probe/sample-k60.log`, `probe/sample-k100.log`.
Reading the seeded samples (three at each k; one cluster appeared at both, so five distinct):
- **Clearly one situation (2):** a spouse scheming against or confronting a spouse over infidelity
(15 scenes, 10 stories); a woman made the centre of attention while men compete for her (5 scenes,
5 stories).
- **Broadly one situation (2):** a secret identity or past revealed in a romantic encounter, often over
dinner (15 scenes, 13 stories); hosts and guests jockeying for social rank at a dinner (6, 6).
- **Not a situation (1):** fortune-tellers, adventurers, a found tyre, a graft inquiry — held together
by "seeks", "fortune" and "mysterious" and dominated by three stories. It is the same cluster at
both k.
The rule as written requires every sample to read as one situation, so **strictly this is not a full
pass**, and it is recorded that way rather than rounded up.
**Follow-up: the selection step is designed and tested in [`SELECTION.md`](SELECTION.md).** A
two-signal gate (cross-story cohesion, and no single word dominating) accepted 10 clusters across
development and test, all of them situations; it rejects the vocabulary clusters this result found.
But it keeps only 5 of ~40 candidates — far short of what a pack needs — so recall is the open problem.
Three recall-recovery methods were then tested on a fresh partition and **none met its pre-registered
bar**: re-clustering found nothing new, consensus clustering found only sharp situations (3 of 3) but
too few, and prune-then-grow found 10 distinct situations with too much noise. The finding that
matters is that the gate is size-dependent in both directions, so it has to be made size-invariant
before recall can be recovered.
A second gate, G2, built for that (cohesion against a same-size random baseline, a significance test
for shared words), **failed validation on fresh data**: it accepts three to four times as many
clusters as the first gate at about the same precision (0.79), but still rejects every cluster under
10 members and still lets growth run to the cap. A fixed threshold on a z-score reverses the size bias
rather than removing it.
A third gate, G3 (a percentile against same-size random sets, plus a mandatory per-member floor),
**failed all four validation criteria and did worse than G2**: 17 accepted across fresh partitions,
only 8 of them situations. The random-set baseline turned out to be the wrong comparison — any
k-means cluster beats random scenes — and the per-member floor is noisiest, and so most lenient,
exactly where clusters are smallest.
A fourth gate, G4, had the inference model read each cluster and say which summaries share one
situation. It **failed with both `qwen2.5:3b-instruct` and `qwen3:14b`** (primary precision 0.48 and
0.52 against G2's 0.73): the models answer with abstractions ("power dynamics", "someone wants
something from someone") that most summaries fit. Observed but not pre-registered: the 14B model AND
G2 accepted 10 clusters, all situations — precise, narrow, and untested on fresh data. The substantive conclusion: summarising
before embedding **does** break the story-identity effect that sank the naive design, and most of
what clusters is recognisably situational — the recurrence premise is alive, not proven. The design
needs a **selection step that rejects vocabulary clusters**, not only a clustering step.
Known defects in this run, none large enough to change the verdict:
- `first_sentence` cut 7 of 838 summaries that begin with an initial ("E. Rushmore Coglan…") to
"E." — single-letter initials and "Gen." are not handled.
- Heavy use of "someone" is itself a shared token across summaries with many characters. Whether it
pulls clusters together has not been measured; a replacement by role or a neutral mix would test it.
- A few names that are also common words survive stripping ("Silver", "Cherry", "Grace", "the Kid").
**Why k ≥ 60, and why a tightness filter.** The first version of this pass mark said "median dominant
≤ 40% at k ≥ 40" — and the prose baseline, the approach known to fail, passed it at k = 40 (36%).
Two things were wrong. Small k forces clusters to merge stories whatever the embeddings encode: on
synthetic vectors carrying nothing but story identity, `measure.py` reports 36% at k = 25, 53% at k
= 40 and 100% at k ≥ 60, while vectors with planted cross-story situations report 11–17% at every k
≥ 40. And without a tightness filter, the loose clusters the 2026-09-10 reading identified as prose
register count as recurring. The tightness cutoff is relative to each file's own median because an
absolute cosine threshold does not transfer between 300-word scenes and one-sentence summaries.
If it fails, the recurrence premise needs rethinking before any design goes further.
### 2. A small model can classify against a closed path vocabulary
Roughly 100+ calls per pack deciding which state paths an option moves and in which direction.
Malformed or wrong output at a meaningful rate changes the design (batching, retries, a bigger
model). **Untested.**
### 3. A small model can substitute entity names without wrecking prose
The main generative task, run on every description. **Untested.**
The summarisation probe in assumption 1 incidentally gives the first evidence on 2 and 3: it asks a
small model for constrained, name-free output and the results can be read.
**First evidence (2026-09-14, `qwen2.5:3b-instruct`, two 40-scene pilots):**
- **It copies examples from the prompt into the output.** A prompt that illustrated roles with "a
clerk", "a landlady", "a rich man", "a city park" got those phrases back 18 times — in scenes
containing **none** of them, every one invented. For this probe that is worse than noise: shared
invented phrases make unrelated scenes cluster together and fake exactly the recurrence being
measured. Removing the examples brought it to 1. **Rule: prompts for a small model name no example
of the output vocabulary.**
- **It does not follow "never use a name."** 25 of 40 summaries kept names with the first prompt,
and 27 of 40 with a stricter one plus an automatic retry — the retry fired 35 times, nearly doubled
the run, and fixed almost nothing. Names are therefore removed **deterministically** afterwards
(`probe/strip_names.py`): a lexicon built from the corpus, of words seen capitalised mid-sentence
and almost never in lowercase. It misses names that are also common words ("Bill", "the Kid",
"New" in "New York"). That bears directly on assumption 3: **entity substitution should be done by
code over a lexicon, with the model's output treated as untrusted.**
- **Throughput is not the constraint.** On a GTX 1080 Ti, a scene summary takes ~0.6 s of model time
(about 1.1 s including a deliberate pause between requests), and embedding runs at ~16 texts/s.
A 838-scene corpus summarises in roughly a quarter of an hour.
## Open questions
- **Description variants need an engine change.** Events carry one description today. Scope it
before building, and record it in theladder's DECISIONS — it is the first engine change content
generation would force.
- **The test strategies hardcode the stat spine.** `theladder/test/fixtures/strategies.js` reads the
literal path `money` and the prefix `relationships.`. A pack that named its currency `coin` would
make four of six archetypes stop discriminating while the sweep still ran and reported. The fixed
spine satisfies this by construction; it is also a coupling that should be made explicit.
- **Options versus variants.** A cluster-derived option is tied to the scene it came from and may
read wrongly under the event's other descriptions. The more variants an event has, the more its
options must come from the fixed archetypes.
- **More options, harder search.** Moving from ~3.1 to ~4 options per event adds roughly 90 effect
magnitudes to a search that already converged only 5 times in 12 with crude repair.
- **Prompts may not transfer between models.** Prompts developed against a capable model are
typically under-specified for a 3B one. The small-model question stays open until it is tested on
a small model, however well a larger one does.
- **Story splitting is per-volume fiddly.** Segmentation needs no model, but each source's format
needs its own pattern. A production tool needs a report when a volume does not split cleanly.
The probe's `split.py` yields 94 stories from 98 contents entries: it silently drops titles
containing quotation marks or an em dash ("LITTLE SPECK IN GARNERED FRUIT", "THE THING'S THE PLAY",
"WHAT YOU WANT", "THE GUILTY PARTY — AN EAST SIDE TRAGEDY"). The 2026-09-10 run dropped the same
ones, so it is left alone to keep the before/after comparison exact.
## Layout
```
story-to-pack/
DESIGN.md this note
recovered/ the 2026-09-10 scripts, as recovered from the transcript; not re-run
probe/ the working probe for assumption 1
corpus/ Gutenberg source texts (rebuildable; see recovered/README.md)
split.py volumes -> stories.json
chunk.py stories -> chunks.json (~300-word scenes)
ollama.py the only module that talks to an inference host
embed.py texts -> vectors, checkpointed
summarise.py scenes -> one generic sentence each, checkpointed
strip_names.py summaries -> summaries with names replaced, from a corpus-built lexicon
measure.py clustering concentration report for any embedding file
sample_clusters.py seeded sample of tight-recurring clusters, for reading (criterion 3)
summaries-v1-pilot.json, summaries-v2-pilot-retry.json the two 40-scene pilots, kept as evidence
*.log run and measurement output
gpu-logs/ the inference host's power/link/kernel logs for the 2026-09-14 run
```
+103
View File
@@ -0,0 +1,103 @@
# story-to-pack
Research toward a tool that builds a The Ladder content pack from a corpus of stories.
**Independent of the game.** Nothing in `src/`, `content/` or `test/` imports anything here, and
nothing here is part of `npm test` or `npm run build`. It lives on the `story-to-pack` branch so it is
versioned alongside the pack format it targets, without touching `main`.
- **What was decided and why:** [`DESIGN.md`](DESIGN.md)
- **How situations are selected, and every test of it:** [`SELECTION.md`](SELECTION.md)
- **The 2026-09-10 scripts, recovered from a transcript:** [`recovered/`](recovered/README.md)
## Status
Selection was settled as **gate G2 followed by a structured human review** (see below). Every automated
alternative tried — three more geometric gates and two model-judged gates — was worse on fresh,
blind-labelled data. The first real review then found G2's groups were **topics** (artists, police,
hotels), not situations. **Now under test:** re-describe every scene as its main person's
**predicament** with qwen3:14b, regroup, and read the groups (SELECTION.md, "Predicaments"). Nothing
downstream of selection (paths and directions, numbers, pack output) is built yet.
## Layout
`probe/` holds the v1 data every G1–G4 result was measured on. `probe/v2/` is the corrected corpus
(97 stories, 839 scenes: v1's splitter merged three quoted-title stories into their neighbours), built by
running the same scripts from inside `v2/` (`python3 ../split.py`, and so on).
```
probe/
corpus/ O. Henry's Four Million cycle, Project Gutenberg (public domain)
split.py, chunk.py volumes -> stories.json -> chunks.json (838 scenes in v1, 839 in v2)
carry_summaries.py v1 summaries onto v2 scenes; only new scene text goes to the model
redescribe.py scene -> its main person's predicament, via the model -> predicaments.json
topic_words.py, topic_share.py how much groups are held together by a job, relationship or place word
pilot_page.py side-by-side page for the 3B / 14B predicament pilot -> pilot-compare.html
groups_page.py reading page for regrouped predicaments -> v2/groups-*.html
ollama.py the only module that talks to an inference host (host from STP_OLLAMA)
summarise.py scene -> one sentence, via the model -> summaries.json
strip_names.py names removed deterministically -> summaries_clean.json
embed.py texts -> vectors -> embeddings.json, summary_embeddings.json
measure.py, signals.py, gate2.py, gate3.py, judge.py … the selection experiments
labels-*.json, sheet-*.txt, candidates-*.json, *-map.json blind labels and what they labelled
review.py G2 on a partition -> REVIEW.html and candidates.json
review_page.py renders the page; picks out role words for each group
review_page.html the page template
review_page_logic.cjs the rules the page applies (also run by the Node tests)
review_format.py the saved review's format, checks and conversion
apply_review.py saved review -> situations.json
test_review.py, test_review_page.mjs tests
review/<run>/ REVIEW.html, candidates.json, and situations.json once applied
```
Not committed (see `.gitignore`): the inference host's power and kernel logs, and caches that the
scripts rebuild on first use (`sim.pkl`, `null*.pkl`, `coassoc.json`). The embeddings are committed
(about 16 MB) because recreating them needs the inference host.
## Running
Everything runs from `probe/` with Python 3 and no third-party packages.
Anything that calls a model reads the host from the environment and never from a file:
```sh
export STP_OLLAMA=http://<inference-host>:11434
```
## Reviewing situations
The review happens in a web page. Nobody edits JSON.
```sh
cd probe
python3 review.py # k = 60, seed 24 -> review/k60-s24/REVIEW.html
```
1. **Open `review/k60-s24/REVIEW.html` in a browser** (double-click it; nothing to install). The first
screen explains the job with examples.
2. **One group per screen.** For each: keep it, drop it, or mark it the same as a group already kept;
untick scenes that don't fit; and fill in "A ___ wants ___ from ___", with the people named in its
scenes offered as click-to-fill words. The page shows as you go whether a group still has enough
scenes, and colours each group in the strip across the top. Keys: K keep, D drop, ← →.
3. **Progress saves in the browser** after every change, so you can close it and come back.
4. **"Finish & save"** lists anything unfinished and saves `review-k60-s24.json`, usually to Downloads.
5. **Then:**
```sh
python3 apply_review.py review/k60-s24
```
It finds the saved review in Downloads by itself, checks everything again, and names any problem the
way the page does ("Group 4: kept, but its name is not finished"). Nothing is written until the
review passes; then it writes **`situations.json`**: each situation's sentence and roles, the scenes
that show it with their stories, and which groups it came from.
`candidates.json` in the run directory is the program's record for the checker; leave it alone.
```sh
python3 -m unittest test_review # the saved review's checks, the page renderer, apply_review.py
node --test test_review_page.mjs # the rules the page applies as you review
```
The page's drawing code is only checked for syntax and for starting up; how it looks and behaves in a
real browser has to be tried by a person.
+891
View File
@@ -0,0 +1,891 @@
# story-to-pack — selecting situations
> **Decision (2026-09-15): selection is gate G2 followed by a structured human review.** `probe/review.py`
> writes `REVIEW.html`, a page where the reviewer keeps, drops or merges each group, unticks scenes and
> names the situation; the page saves a review file, and `probe/apply_review.py` checks it and writes
> `situations.json` for the next stage. See the README.
>
> The first version asked the reviewer to read a long Markdown file and edit JSON by hand. The first
> real attempt stopped before a single entry: what to decide was unclear, there was too much to read at
> once, hand-edited JSON broke easily, and the wording was hard to write. The page answers each of those.
> The record below is how the G2 decision was reached: four gate designs and two model-judged gates,
> each tested against blind labels.
>
> **Superseded in part (2026-09-15), see "Predicaments" at the end:** reading the page, the reviewer
> found the groups were topics, not situations.
The step after clustering: from the clusters of summarised scenes, choose the ones that are genuine
**recurring situations** — the raw material of a recurring event — and reject the rest. Companion to
`DESIGN.md`; this is the step its assumption-1 result called for.
**Status (2026-09-14): designed and pre-registered below, before any feature was computed or any
label was read against a feature.** Results are appended at the end, and nothing above the Results
heading is edited after the test runs, except to fix a typo.
---
## The problem, from the assumption-1 result
Summarise-then-embed broke the story-identity effect: clusters now draw from many stories. But of five
sampled tight-recurring clusters, one was a **vocabulary cluster** — fortune-tellers, adventurers, a
found tyre, a graft inquiry — held together by "seeks", "fortune", "mysterious", and mostly from three
stories. The concentration and tightness filters in `measure.py` let it through. A selector has to
reject clusters like that one and keep clusters like "a spouse schemes against or confronts a spouse
over infidelity".
## Constraints
- **No inference.** Selection uses only what is already on disk: the summary embeddings, the prose
embeddings, the summary text, and which story each scene came from. It is pure computation, like the
numeric search, so it can be re-run and tuned for free. (It also keeps the inference host free for
other work.)
- **Deterministic.** Seeded clustering; the same inputs give the same selection.
- **Pure Python**, like the rest of the probe.
## What a selected situation is
A set of scenes, from **at least 4 different stories**, with no story contributing more than 40%,
that a reader can name as **one social transaction**: who wants what from whom. It becomes one
recurring event; its member scenes become that event's description variants and option material.
Rejected: story-dominated clusters (already mechanical), **vocabulary clusters** (shared words, no
shared transaction), **register clusters** (shared tone or setting, e.g. "men walking through the
city"), and **near-duplicates** of an already-selected situation.
## Candidate signals
All computed per cluster, from existing vectors and text. Each carries a pre-registered prediction of
which way it moves for a genuine situation.
| # | Signal | Definition | Prediction for a situation |
| --- | --- | --- | --- |
| S1 | **Cross-story cohesion** | Mean summary-embedding cosine over member pairs from *different* stories | Higher |
| S2 | **Cross/within ratio** | S1 divided by the mean cosine over member pairs from the *same* story (1.0 if there are none) | Closer to 1 — a situation is as similar across stories as within one; a cluster held together by a few stories' scenes is not |
| S3 | **Effective stories** | Inverse Simpson index of the member story counts, divided by cluster size | Higher — members spread evenly rather than a few stories plus stragglers |
| S4 | **Prose agreement** | Mean *prose*-embedding cosine over cross-story member pairs, minus the corpus-wide mean cross-story prose cosine | Higher — a second, independent view: scenes of the same situation should read a little alike even in the original text |
| S5 | **Stability** | Mean pairwise co-association of members across 12 k-means runs (k from 50 to 100, several seeds): the share of runs in which two members land together | Higher |
| S6 | **Word dependence** | Share of members containing the cluster's single most over-represented content word (log-odds against the whole summary set, stopwords and "someone" excluded) | Higher for a **vocabulary** cluster — one word doing the work |
S6 is predicted to indicate the failure, the others to indicate success.
## Protocol
1. **Candidates.** Cluster the summary embeddings with k = 60. A candidate is a cluster with ≥ 5
members, ≥ 4 distinct stories and dominant share ≤ 40% (`measure.py`'s "recurring"; the relative
tightness filter is *not* applied — the selector replaces it).
2. **Two candidate sets from two partitions.** Development: seed 11. Test: seed 12.
3. **Blind labels.** Each set is dumped to a sheet showing, per cluster, only its member summaries in
shuffled order — no story titles, no scores, no sizes beyond the lines themselves. Each cluster is
labelled by reading:
- **2 — one situation:** one nameable social transaction that most members (roughly 70%+)
instantiate. The label records its name.
- **1 — broad:** a shared setting or theme, but the transaction varies across members.
- **0 — not a situation:** vocabulary, register, or nothing nameable.
The test sheet is labelled before any signal is computed for the test set.
4. **Choose the rule on development only.** Compute S1–S6 for the development set. Candidate rules
are single thresholds on one signal, or an AND of two. The chosen rule is the one with the highest
**precision** (share of accepted clusters labelled 2), subject to accepting at least half of the
development set's label-2 clusters; ties go to the simpler rule. Freeze it here, in writing, before
step 5.
*Detail fixed after the labels were written and before any signal was computed:* thresholds are
placed midway between adjacent observed development values, never on one. Ties beyond simplicity
break on more label-2 clusters accepted, then on precision for label ≥ 1. Two reference points
are reported but cannot be chosen: accepting every candidate, and `measure.py`'s tightness filter
(coherence at or above the partition's median sized cluster), called B0.
5. **Test once.** Apply the frozen rule to the test set. Report precision (label 2, and label ≥ 1),
recall of label-2 clusters, and the number accepted. No re-tuning after seeing test results; any
later change is a new rule and needs new data to be tested on.
6. **Near-duplicates.** Among accepted clusters, merge any pair whose centroids have cosine ≥ a
threshold chosen on development by reading the labels' names. Report how many merges happened on
the test set and whether each merged pair's names agree.
## Known limits of this test
- **The labels are one reader's judgement** — the model running the probe. They are written to a
file so the designer can spot-check or overrule them, and the verdict should be read with that in
mind.
- **Development and test come from the same corpus.** Two partitions of the same 838 scenes share
situations, so the test measures stability across clustering, not generalisation to a new corpus.
A genuinely held-out test needs a second corpus, which needs embedding, which needs the inference
host.
- **k is fixed at 60** for the test. The selector's behaviour at other k is not tested.
## Frozen rule (written before any test signal was computed)
Chosen on development (40 candidates, 5 labelled 2; must accept ≥ 3), by `probe/select_rule.py choose`:
> **Accept a candidate when S1 ≥ 0.6119 AND S6 ≤ 0.1603.**
> Cross-story cohesion at least 0.6119, and no single over-represented word in more than 16% of members.
| On development | Accepted | Precision (2) | Precision (≥ 1) | Recall (2) |
| --- | --- | --- | --- | --- |
| Accept every candidate | 40 | 0.12 | 0.62 | 1.00 |
| B0, tightness filter | 17 | 0.24 | 0.76 | 0.80 |
| S1 alone (best single) | 9 | 0.44 | 0.89 | 0.80 |
| **Frozen rule** | **5** | **0.60** | **1.00** | **0.60** |
Accepted on development: D04 (2), D09 (1), D13 (2), D16 (2), D33 (1). Rejected label-2: D15 (S1 0.603),
D30 (S6 0.20). Full leaderboard: `probe/choose-dev.log`; signals: `probe/signals-dev.json`.
Observations recorded now, so they cannot be shaped by the test:
- **This is very likely overfit.** Five positives, fine-grained thresholds, and an AND of two signals
is exactly the setting in which a rule memorises its development set. The test is what decides.
- **S1 does most of the work.** It is the best single signal by a wide margin, and it alone rejects
the vocabulary cluster that motivated this step (D18, S1 0.605).
- **S6 did not behave as predicted.** Its most over-represented word is usually a *story's* word
("juggler", "furs", "eviction"), not generic vocabulary, and D18's is low (0.11). It catches D39
("young", 1.00) — a vocabulary cluster of a different kind. Its contribution to the frozen rule is
a single cut, and may not carry over.
- **S2, S3 and S4 were weak on development**, and S5 middling. None of the predicted directions was
strongly wrong, but only S1 separated well.
**Near-duplicates (step 6): centroid cosine cannot do it, so no automatic merge is applied.** Among
the five clusters the rule accepts on development, no two share a situation, so there was nothing
there to calibrate on. Across all 26 development candidates labelled ≥ 1, the one clear duplicate —
D13 and D30, both "a run-in with the police" — has centroid cosine **0.9026**, while five pairs of
plainly different situations score higher, up to **0.9326** ("money and marriage" against "schemes,
cons and theft"). D05/D23 (artists) sit at 0.9005. Any threshold that merges the duplicate merges
different situations first. Summary embeddings place situations about money, romance and conflict
in one dense region, so closeness between centroids is not identity of situation.
Decision, fixed before the test: the test reports, **by label name**, whether any two accepted
clusters are the same situation, as a measurement of how often duplicates survive selection. A
merge method is an open problem; member overlap across partitions (co-association between two
clusters' members) is the candidate to try next, and is untested.
## Results
**Test run once, 2026-09-14,** on the seed-12 partition (41 candidates, 5 labelled 2), frozen rule
unchanged. Logs: `probe/signals-test.json`, `probe/apply-test.log`.
| On test | Accepted | Precision (2) | Precision (≥ 1) | Recall (2) |
| --- | --- | --- | --- | --- |
| Accept every candidate | 41 | 0.12 | 0.51 | 1.00 |
| B0, tightness filter | 18 | 0.17 | 0.44 | 0.60 |
| **Frozen rule** | **5** | **0.40** | **1.00** | **0.40** |
Accepted: T35 (2) coercion over money; T39 (2) spouse against spouse over infidelity; T05 (1) schemes
and cons over money; T07 (1) courtship; T25 (1) trying to impress or win someone's favour.
Rejected label-2: T16 newcomer seeks acceptance (S1 0.598), T17 proposal or engagement (S1 0.596),
T30 run-in with the police (S1 0.604, S6 0.23).
### Verdict
**The selector is precise and narrow.** Across both sets it accepted 10 clusters, and **all 10 are
situations or broad situational themes; none is a vocabulary, register or grab-bag cluster.**
Accepting everything gives about half that. The tightness filter used in the assumption-1 test is
*worse* than accepting everything on the test set (0.44 against 0.51) — it keeps vocabulary clusters,
which are tight.
**It does not find most of the situations.** Recall of label-2 clusters fell from 0.60 to 0.40, and
precision for label 2 from 0.60 to 0.40 — the expected overfitting. The rejected label-2 clusters sit
just under the S1 cut (0.596–0.604), so the threshold is doing real work at a place where situations
and non-situations overlap. **Five accepted clusters out of 41 is far short of the ~15 situations
per stage a pack needs**, so as it stands selection would make the sufficiency report fail on this
corpus.
**S6 earned its place on the test set, in the way it failed to on development.** Two test clusters are
held together by a single word — T10 "caliph" (S6 0.70) and T18 "young" (1.00) — and both have high
cross-story cohesion (S1 0.628, 0.626). S1 alone would accept them; S6 rejects them. (Observation made
after the test, not a re-tuning: S1 ≥ 0.6119 alone would accept 10 test clusters, 2 labelled 2 and 6 labelled ≥ 1.)
Its original motivation, D18, was caught by S1 instead. Both failure modes exist, and each signal
catches one.
**Duplicates among accepted clusters:** no two carry the same label name. T07 (courtship) and T25
(trying to impress or win someone's favour) overlap in substance; a reader could reasonably call them
one situation. Automatic merging remains unsolved (see step 6).
### What this means for the design
1. **Two-signal selection is a sound first gate, not a complete step.** It should run as a *filter
that is allowed to reject* — its precision is what matters for pack quality — followed by a
second stage that recovers recall.
2. **Recall is the next problem, and clustering is the likely cause, not selection.** The rejected
situations (police run-ins, proposals, social climbing) are recognisable but diluted by
off-situation members at k = 60. Candidates to test, all without inference: re-cluster the members
of rejected clusters at finer k and re-apply the gate; select from the union of several partitions,
using co-association to keep only members that consistently travel together; or grow an accepted
cluster's core by adding nearest cross-story members while S1 stays above the cut.
3. **Merging duplicates needs member overlap, not centroid distance.**
4. **These labels are one reader's.** Every number above rests on them. They are in
`probe/labels-dev.json` and `probe/labels-test.json`, with the sheets they were read from, and are
worth a designer's spot-check — especially the 1-versus-0 boundary, which is the least certain.
5. **The test is within one corpus.** Generalisation to a different corpus is untested and needs
embeddings, which needs the inference host.
---
## Recovering recall (pre-registered 2026-09-14, before any recovery method was run)
The gate is precise and keeps too few. Three ways to find more situations, compared on **fresh data**,
because the test set above is spent. Still no inference.
### Fixed before running
- **The gate does not change.** `S1 ≥ 0.6119 AND S6 ≤ 0.1603`, plus the candidate conditions (≥ 5
members, ≥ 4 stories, dominant ≤ 40%). "Passes the gate" below means all of these.
- **A property of the gate, noted in advance:** S6 counts a word only if at least two members share
it, so a cluster of fewer than 13 members fails whenever any content word repeats (2/12 = 0.167). In
effect the gate requires ~13 members. The methods are built to produce clusters of that size;
anything smaller is expected to fail, and that is the gate's behaviour, not a method's.
- **Fresh partition:** k = 60, **seed 13** — neither development (11) nor test (12).
- **Baseline (B):** the gate applied to seed 13's candidates, as before.
- **M1 — re-cluster the rejected.** Every scene not in a B-accepted cluster is pooled and re-clustered
with k-means, k = round(pool size / 20), seed 13. Result: B's accepted clusters plus pool clusters
that pass the gate.
- **M2 — consensus clustering.** Each scene is represented by its co-association row over the 12 cached
runs (k 50–100, seeds 21 and 22 — no overlap with 11, 12 or 13): how often it landed with every
other scene. Those rows are clustered with k-means, k = 60, seed 13. Result: clusters that pass the gate.
- **M3 — prune, then grow.** For each seed-13 candidate: if it fails the gate, repeatedly remove the
member with the lowest mean similarity to members from *other* stories, until it passes or falls to
13 members (then it is dropped). Then grow every passing cluster: add the non-member scene nearest
its centroid while the result still passes; stop at the first addition that would fail, or at 30
members. Scenes may end up in more than one M3 cluster; the overlap is reported.
### Evaluation
- Every cluster accepted by any method is pooled (identical member sets once), given a shuffled id, and
dumped to a blind sheet showing only member summaries. Which method produced a cluster is written
to a separate map file that is not read until labelling is finished.
- Labels use the same 2 / 1 / 0 scale. Names are reused verbatim from the development and test label
files wherever the situation is the same, so duplicate situations can be counted by name.
- Per method: clusters accepted; precision for label ≥ 1 and for label 2; **distinct situations**
(distinct names among label ≥ 1); scene coverage; overlap between accepted clusters.
- **Success, per method:** at least **twice** B's accepted clusters, **and** precision (≥ 1) of at least
**0.80**, **and** at least twice B's distinct situations. For scale: a two-stage pack wants ~30.
### Known limits
The reader has now read every summary several times; labels are blind to method and signals, not to
content. The methods and the gate are evaluated on one corpus.
### Results (2026-09-14)
Seed 13 gave 42 candidates. 21 distinct accepted clusters were labelled blind (`probe/labels-recover.json`,
3 labelled 2, 13 labelled 1, 5 labelled 0), then scored by `probe/evaluate_recover.py`:
| Method | Accepted | Precision (≥ 1) | Precision (2) | Distinct situations | Scene coverage | Success |
| --- | --- | --- | --- | --- | --- | --- |
| B, gate on candidates | 4 | 1.00 | 0.00 | 4 | 13% | — |
| M1, re-cluster the rejected | 4 | 1.00 | 0.00 | 4 | 13% | **fail** — 0 new |
| M2, consensus clustering | 3 | 1.00 | **1.00** | 3 | 5% | **fail** — too few |
| M3, prune then grow | 16 | 0.69 | 0.00 | 10 | 45% | **fail** — precision |
**No method meets the pre-registered bar.**
- **M1 adds nothing.** Re-clustering the 729 scenes outside B's clusters at k = 36 produced no cluster
that passes the gate. The gate's selectivity does not come from dilution by one bad partition.
- **M2 is the only method that produced sharp situations.** All three of its clusters are labelled 2
— a wealthy suitor weighed against love, a spouse against a spouse over infidelity, coercion by
threat — at 14–17 members, against B's 13–38. Consensus over many partitions isolates the scenes
that genuinely travel together. It produces too few clusters to be the selection step on its own.
- **M3 finds the most, and the gate cannot stop it diluting.** Pruning recovered 12 rejected
candidates and M3 reached 10 distinct situations (B: 4), but 5 of its 16 clusters are not
situations. **Growth was stopped by the 30-member cap in 10 of 16 clusters, not by the gate.**
That exposes the gate's second size effect: S6 is a share, so it gets *easier* to satisfy as a
cluster grows, and S1 moves slowly. The gate calibrated on k = 60 clusters does not constrain a
cluster that is being built up one scene at a time.
- **Duplicates survive.** Across methods, "a spouse against a spouse" appears three times (R006, R015)
and courtship twice within M3 (R008, R010); 68 scenes sit in more than one M3 cluster.
- **The seed-13 baseline itself accepted no label-2 cluster** (0 of 4), where development and test
each had 2–3. The baseline's precision for label 2 is unstable across partitions.
### What this means
The gate, as frozen, is **size-dependent in both directions**: it structurally rejects clusters under
~13 members and barely constrains clusters above ~25. Any method that changes cluster size — pruning,
growing, finer re-clustering — changes what the gate measures. Every recovery method was, in effect,
testing the gate at sizes it was not calibrated for.
Observation only, not pre-registered and not a result: B and M2 together would give 7 clusters,
6 distinct situations, all labelled ≥ 1, and 3 labelled 2.
Directions this points to, none tested:
1. **Make the gate size-invariant before anything else.** Replace S6's share with a count or a
significance test against the corpus, and judge S1 relative to what a random cluster of the same
size scores. Then re-validate — which needs fresh labels on fresh partitions.
2. **Use consensus (M2) to form clusters, not k-means.** It was the only source of sharp situations.
Running more co-association partitions (more seeds, more k) and extracting cores at several
agreement levels may raise its yield without losing precision.
3. **Grow only with a size-invariant stopping rule.** M3's recall is real; its stopping rule is not.
---
## A size-invariant gate, G2 (pre-registered 2026-09-14, before any G2 signal was computed)
### Why each old signal depended on size
- **S6** is a share with a floor of 2 members: 2/size. Under ~13 members any repeated word fails the
cut; above ~25 almost nothing does. A share is not evidence unless the count is improbable.
- **S1**'s *expectation* does not depend on size — it is a mean over pairs — but its *noise* does:
small clusters reach a high S1 by chance, which is part of why k-means' small clusters look tight.
- **Nothing looked at individual members.** Adding one off-situation scene to 30 moves S1 by
roughly a thirtieth of the difference, so growth was never stopped.
### G2 signals
| Signal | Definition | Why it is size-invariant |
| --- | --- | --- |
| **S1** | Unchanged: mean summary cosine over cross-story member pairs | Unbiased at any size |
| **Z1** | (S1 − μₘ) / σₘ, where μₘ and σₘ come from random member sets of the same size m (≥ 4 stories, dominant ≤ 40%): 300 samples for m ≤ 40, 120 above, sizes 5–150, seeded | Requires more evidence where chance does more |
| **MMIN** | The lowest, over members, of a member's mean cosine to members of *other* stories | A per-member quantity; one diluting member fails it at any size |
| **W** | Among words shared by ≥ 2 members, those whose count is significant by a hypergeometric upper-tail test at Bonferroni 0.01 over the corpus vocabulary; W is the largest share among them (0 if none) | Counts only improbable sharing, then measures how dominant it is |
Word extraction, stopwords and the corpus are identical to `signals.py`. Pairwise similarities are
precomputed once. Candidate conditions (≥ 5 members, ≥ 4 stories, dominant ≤ 40%) still apply.
### Choosing thresholds
- **Tuning set:** every labelled cluster so far — development (40), test (41) and the recovery pool
(21): 102 clusters, 63 labelled ≥ 1, sizes 5–39. All have been seen; none is used for validation.
- **Rule form:** S1 ≥ a AND Z1 ≥ b AND MMIN ≥ c AND W < d, where each term may be switched off. Grid:
a and c at the deciles of the tuning set's values or off; b ∈ {off, 1, 2, 3, 4, 5}; d ∈ {off, 0.25,
0.30, 0.35, 0.40, 0.50, 0.60}.
- **Objective:** the most tuning clusters labelled ≥ 1 accepted, subject to precision (≥ 1) of at least
**0.85**; ties on more label-2 accepted, then fewer active terms. The objective changes from the
first gate (which maximised label-2 precision) because the failure now is recall, and label 1 —
a situational theme — is usable material for a pack.
- The tuning report also gives, per size band (≤ 12, 13–25, ≥ 26), how many clusters the old and new
gates accept and at what precision.
### Validation on fresh data (nothing below has been computed)
- **V1, small clusters:** k = 100, seed 15; 30 candidates sampled at random (seed 15).
- **V2, large clusters:** k = 40, seed 14; 30 candidates sampled at random (seed 14).
- **V3, growth:** k = 60, seed 16; every candidate G2 accepts is grown by adding the non-member nearest
its centroid while the result still passes G2, up to 45 members. The stop reason — gate or cap — is
recorded.
- All V1 and V2 samples and all V3 grown clusters are pooled, deduplicated, shuffled and labelled blind,
with the set and method in a separate map file.
- **Success, all three required:**
1. G2's precision (≥ 1) is at least **0.80 on V1 and on V2 separately**, and G2 accepts at least 3
clusters in each.
2. In V1, G2 accepts **more** small clusters than the old gate.
3. In V3, **most growths are stopped by the gate, not the cap**, and grown clusters have precision
(≥ 1) of at least 0.80.
The old gate is scored on V1 and V2 alongside, for comparison.
### Frozen G2 (written after tuning, before any validation cluster was built)
`probe/tune_gate2.py`, log `probe/tune-gate2.log`. (Correction: the tuning set has 62 clusters
labelled ≥ 1, not 63.)
> **Accept a candidate when S1 ≥ 0.6037 AND Z1 ≥ 5 AND W < 0.60.** MMIN is off.
| Tuning set, 102 clusters | Accepted | Label ≥ 1 | Precision (≥ 1) | Label 2 |
| --- | --- | --- | --- | --- |
| Accept all | 102 | 62 | 0.61 | 13 |
| Old gate | 31 | 26 | 0.84 | 8 |
| **G2** | **40** | **34** | **0.85** | **9** |
| Size band | Clusters (≥ 1) | Old gate: accepted / P1+ | G2: accepted / P1+ |
| --- | --- | --- | --- |
| ≤ 12 | 20 (7) | 0 / — | 1 / 1.00 |
| 13–25 | 52 (34) | 13 / 0.92 | 18 / 0.89 |
| ≥ 26 | 30 (21) | 18 / 0.78 | 21 / 0.81 |
**Observations and predictions, recorded before validation:**
- **G2 is not size-invariant at the small end, and the objective chose that.** Z1 ≥ 5 is a fixed bar
on a statistic whose value, for a fixed true cohesion, *grows* with size (σₘ shrinks as m grows).
It replaces S6's structural rejection of small clusters with a milder one: 1 small cluster accepted
instead of 0. **Prediction: criterion 2 passes narrowly or fails, and V1 yields fewer than 3
accepted clusters, failing criterion 1.**
- **MMIN, the signal designed to stop dilution, was not selected**, and W's cut (0.60) is loose enough
to act only on near-total single-word clusters. Growing a cluster raises Z1 and barely moves S1.
**Prediction: most V3 growths run to the cap, failing criterion 3.**
- The gain over the old gate is real but modest, and it is in the mid and large bands: 8 more clusters
labelled ≥ 1 accepted at unchanged precision.
- The thresholds are not changed in light of these predictions. The validation tests the rule the
pre-registered procedure produced.
### Validation results (2026-09-14)
73 fresh clusters labelled blind (`probe/labels-validate.json`: 6 labelled 2, 33 labelled 1, 34
labelled 0), scored by `probe/validate_gate2.py score` (log `probe/validate-score.log`).
| Set | Sampled (≥ 1) | Old gate: accepted / ≥ 1 / P1+ | G2: accepted / ≥ 1 / P1+ |
| --- | --- | --- | --- |
| V1, k = 100 | 30 (13) | 3 / 2 / 0.67 | **10 / 8 / 0.80** (2 labelled 2) |
| V2, k = 40 | 30 (16) | 1 / 1 / 1.00 | 4 / 3 / 0.75 |
| V3, grown | 13 | — | 13 grown: **11 stopped by the cap**, 2 by the gate; 10 ≥ 1, P1+ 0.77 |
| Criterion | Result |
| --- | --- |
| 1. P1+ ≥ 0.80 and ≥ 3 accepted, on V1 and V2 | **fail** — V1 passes (0.80, 10), V2 does not (0.75, 4) |
| 2. G2 accepts more V1 clusters than the old gate | **pass** — 10 against 3 |
| 3. V3 mostly stopped by the gate, P1+ ≥ 0.80 | **fail** — 2 of 13, 0.77 |
| **Overall** | **fail** |
By size, across V1 and V2 together:
| Size | Old gate: accepted (≥ 1) | G2: accepted (≥ 1) |
| --- | --- | --- |
| ≤ 9 | 0 | **0** — including two clusters labelled 2 (5 and 6 members) |
| 10–12 | 0 | 3 (3) |
| 13–25 | 3 (2) | 7 (5) |
| ≥ 26 | 1 (1) | 4 (3) |
| All | 4 (3), P1+ 0.75 | 14 (11), P1+ 0.79 |
### Verdict on G2
**G2 is a real improvement on the old gate, and it is not size-invariant.** It accepts three to four
times as many clusters at slightly better precision, and it opens the 10–25 member range the old gate
all but closed. Both pre-registered predictions of failure held:
- **The small end is still closed below 10 members.** Z1 ≥ 5 is unreachable for a cluster of 5–9
however cohesive it is. The two sharpest small clusters on the sheet — a fare dispute (5 members,
label 2) and a spouse against a spouse (6 members, label 2) — were both rejected.
- **Growth is still unconstrained.** Growing a cluster raises Z1 and barely moves S1, so both gates
accept a 45-member cluster as readily as a 15-member one. The one per-member signal built to stop
this, MMIN, was not selected by the tuning objective.
- **Large clusters are still mostly rejected** (4 of 30 in V2), including clear themes such as the
saloon, dining rooms and illness.
**What was learned about size-invariance itself:** a fixed threshold on a z-score is not size-invariant;
it reverses the old bias rather than removing it. A size-invariant cohesion test needs a threshold
that scales with the null — for example a fixed *percentile* of the same-size random distribution
combined with an effect-size floor on S1 — and a stopping rule for growth has to be a per-member
test that the tuning cannot switch off. Both are untested proposals, and testing them needs another
fresh partition and another round of blind labels, because every cluster labelled so far has now
informed a design decision.
---
## G3: a percentile of the same-size null, and a mandatory member floor (pre-registered 2026-09-14, before any G3 signal was computed)
### Changes from G2
- **Z1 is replaced by PCT**: the fraction of same-size random member sets (≥ 4 stories, dominant ≤ 40%)
whose S1 is below the cluster's. 1,000 samples per size for 5–60 members, 200 for 61–150, seeded.
A percentile threshold asks "is this cluster as unusual as the top x% of random sets *of its own
size*", which a z-score with a fixed bar does not.
- **S1 floor is mandatory.** At large sizes the null is narrow and PCT saturates at 1.0 for any
cluster slightly above average, so an effect-size floor has to carry the weight there.
- **MMIN is mandatory**: every member's mean cosine to members of other stories must reach the floor.
It is the growth stop; tuning chooses its value but cannot switch it off.
- **W** is unchanged from G2 and remains optional.
### Tuning
- **Tuning set: all 175 labelled clusters** — development 40, test 41, recovery 21, G2 validation 73.
- **Rule form:** S1 ≥ a AND PCT ≥ p AND MMIN ≥ c AND W < d. a and c at the tuning set's deciles (no "off");
p ∈ {0.90, 0.95, 0.975, 0.99, 0.999}; d ∈ {off, 0.30, 0.40, 0.50, 0.60}.
- **Objective unchanged from G2:** most clusters labelled ≥ 1 accepted, subject to precision (≥ 1) of at
least 0.85; ties on more label-2 accepted, then fewer active terms.
**Risk named in advance:** MMIN is a minimum over per-member means. Small clusters have noisier
member means but fewer of them; large clusters have steadier means but more chances to contain one
low member. Its net size effect is not predictable, and it could re-introduce a size bias of its own.
### Validation on fresh partitions (seeds never used before)
- **V0, very small:** k = 150, seed 20; 25 candidates sampled (seed 20).
- **V1, small:** k = 100, seed 17; 20 sampled (seed 17).
- **V2, large:** k = 40, seed 18; 20 sampled (seed 18).
- **V3, growth:** k = 60, seed 19; every G3-accepted candidate grown toward its nearest scene while it
passes G3, cap 45, stop reason recorded.
- Pooled, deduplicated, shuffled and labelled blind as before. The old gate and G2 are scored on the
same clusters.
**Success, all four required:**
1. G3 precision (≥ 1) ≥ **0.80** and ≥ 3 accepted, on V1 and on V2 separately.
2. **Small clusters:** among V0 and V1 clusters of ≤ 9 members, G3 accepts **at least 2**, with precision
(≥ 1) of at least **0.67**, and more than G2 accepts.
3. **Growth:** most V3 growths stopped by the gate, and grown clusters have precision (≥ 1) ≥ 0.80.
4. G3 accepts at least as many clusters labelled ≥ 1 across V0–V2 as G2, at precision no lower than G2's.
### Frozen G3 (written after tuning, before any validation cluster was built)
`probe/tune_gate3.py`, log `probe/tune-gate3.log`.
> **Accept a candidate when S1 ≥ 0.6175 AND PCT ≥ 0.90 AND MMIN ≥ 0.5296 AND W < 0.60.**
| Tuning set, 175 clusters (101 ≥ 1, 19 label 2) | Accepted | Label ≥ 1 | Precision (≥ 1) | Label 2 |
| --- | --- | --- | --- | --- |
| Old gate | 47 | 38 | 0.81 | 9 |
| G2 | 67 | 55 | 0.82 | 12 |
| **G3** | **47** | **40** | **0.85** | **13** |
| Size band | Clusters (≥ 1) | Old: accepted/≥ 1 | G2 | G3 |
| --- | --- | --- | --- | --- |
| ≤ 9 | 26 (5) | 0/0 | 0/0 | **4/3** |
| 10–12 | 21 (11) | 0/0 | 4/4 | 4/4 |
| 13–25 | 75 (47) | 17/15 | 26/22 | 17/17 |
| ≥ 26 | 53 (38) | 30/23 | 37/29 | 22/16 |
**Observations and predictions, recorded before validation:**
- **G3 is the first gate to accept any cluster under 10 members** (4 on tuning, 3 of them ≥ 1).
**Prediction: criterion 2 passes.**
- **The percentile threshold settled at its loosest value, 0.90, and the S1 floor rose** from G2's 0.6037
to 0.6175. The floor, not the percentile, is doing most of the work at mid and large sizes, and G3
accepts 15 fewer large clusters than G2 on tuning. **Prediction: criterion 4 fails** — G3 finds fewer
clusters labelled ≥ 1 than G2 (40 against 55 on tuning), though at higher precision. Criterion 1 is
at risk on V2 for the same reason.
- **MMIN at 0.53 is the growth stop.** Whether a nearest-to-centroid scene falls below it is not
predictable from tuning, which contains no grown-under-G3 clusters. No prediction for criterion 3.
- The trade is visible already: G3 exchanges recall at the large end for precision everywhere and for
admission at the small end. Whether that is the right trade for a pack is a design question the
validation cannot answer.
### Validation results (2026-09-14)
75 fresh clusters labelled blind (`probe/labels-validate3.json`: 4 labelled 2, 24 labelled 1, 47
labelled 0), scored by `probe/validate_gate3.py score` (log `probe/validate3-score.log`).
| Set | Sampled (≥ 1) | Old: acc / ≥ 1 / P1+ | G2: acc / ≥ 1 / P1+ | G3: acc / ≥ 1 / P1+ |
| --- | --- | --- | --- | --- |
| V0, k = 150 | 25 (5) | 2 / 1 / 0.50 | 8 / 4 / 0.50 | 11 / 4 / 0.36 |
| V1, k = 100 | 20 (5) | 1 / 0 / 0.00 | 1 / 1 / 1.00 | 4 / 2 / 0.50 |
| V2, k = 40 | 20 (12) | 0 / 0 / — | 4 / 4 / 1.00 | 2 / 2 / 1.00 |
| ≤ 9 members, V0 + V1 | 27 (4) | 1 / 0 / 0.00 | 3 / 1 / 0.33 | 11 / 3 / 0.27 |
| V0–V2 pooled | 65 (22) | 3 / 1 / 0.33 | **13 / 9 / 0.69** | 17 / 8 / 0.47 |
| V3, grown under G3 | 10 | — | — | 7 stopped by the cap, 3 by the gate; P1+ 0.60 |
**All four criteria fail**, and G3 is worse than G2 on fresh data: more clusters accepted, fewer of
them situations. G3's gain on the tuning set (P1+ 0.85, 13 label-2) did not survive a new partition.
### Why G3 failed — measured, not inferred
- **PCT carries no information.** Across all 175 tuning clusters, 173 reach PCT ≥ 0.90 and 141 reach
1.0 — including 72 of the 74 clusters labelled 0. **The random-set null is the wrong null.** k-means
groups each scene with its nearest neighbours, so every cluster it produces is far tighter than a
random set of scenes of the same size, whether or not it is a situation. A cohesion test has to ask
whether a cluster is tighter than *other clusters of its size*, not than random scenes.
- **MMIN barely separates labels and favours small clusters.** Median MMIN is 0.536 for label 0,
0.543 for label 1 and 0.564 for label 2; by size it is 0.547 for ≤ 9 members, 0.537 for 10–25 and
0.552 for ≥ 26. A minimum over few, noisy per-member means is not reliably lower for small clusters,
so the floor admitted 11 small clusters of which 8 are not situations. This is the risk recorded
before tuning.
- **Growth is still not stopped by the gate** (7 of 10 to the cap). A nearest-to-centroid scene
typically clears a member floor calibrated on whole clusters.
### Where this leaves selection
Across three gates and five blind-labelled evaluations, **G2 remains the best**: pooled precision
(≥ 1) of 0.79 on its own validation and 0.69 here, with the fewest non-situations admitted.
None of the three is size-invariant, and none stops growth.
Two conclusions look robust enough to build on:
1. **Cohesion alone does not identify a situation.** Clusters labelled 0, 1 and 2 overlap heavily on
every cohesion statistic tried (S1, Z1, PCT, MMIN). What separates the best-labelled clusters is
visible to a reader — one transaction — and is not a geometric property of summary embeddings at
the resolution tried.
2. **The labelling budget is the binding constraint.** Each gate needs fresh partitions and 70–80 new
blind labels to test honestly, and every labelled cluster so far has informed a design. Continuing
to iterate gate designs on this corpus without a different kind of evidence is unlikely to converge.
Candidate directions, none tested: a null built from **k-means clusters of the same size on
shuffled-story data** rather than random scenes; a selection step that uses the **inference model to
name each cluster's transaction** and checks agreement across members (needs the GPU); or accepting
G2 as the gate and moving the problem downstream to event construction, where a human pass over
~40 candidate clusters is cheap compared with the labelling this probe has already required.
---
## G4: the model reads the cluster (pre-registered 2026-09-14, before any model call)
The finding above is that what makes a cluster a situation is visible to a reader and not to the
geometry. So let a reader decide — the inference model — and measure it against the blind labels.
### Method
- For each cluster, up to **15 member summaries** (a seeded sample when larger), shuffled and numbered,
go to the model in one call with the prompt below. It returns JSON: the single social transaction
most of them clearly show, or NONE, and the numbers of the summaries that clearly show it.
- **FIT** = (number of valid summary numbers listed) / (number shown); 0 when the answer is NONE or
the reply cannot be parsed. Parse failures are counted and reported.
- **Gate: accept when FIT ≥ θ**, plus the usual candidate conditions (≥ 5 members, ≥ 4 stories,
dominant ≤ 40%), which every labelled cluster already meets.
- Deterministic: temperature 0, 2,048-token context, `format: json`. One request at a time, as before.
- **The prompt contains no example of a situation** — the 3B model copied the examples it was given
in the summarisation pilot.
Prompt (system), fixed:
> You read short summaries of scenes from different stories and decide whether they share one kind
> of social situation.
>
> A social situation is a specific transaction between people: who wants what from whom, and what
> is at stake. Describe it in your own words.
>
> Rules:
> - Find the single situation that the largest number of summaries clearly show.
> - It is normal for many summaries not to fit. Include a summary only if it clearly shows that
> situation.
> - A shared place, mood, profession or word is not a situation.
> - If no situation is clearly shown by at least three summaries, the situation is NONE.
>
> Reply with JSON only: {"situation": "<at most 10 words, or NONE>", "fits": [<numbers of the
> summaries that clearly show it>]}
The user message is the numbered list. For `qwen3:14b` it ends with `/no_think`, and any think block
is stripped before parsing.
### Models
- **Primary: `qwen2.5:3b-instruct`** — the design requires a small local model; the verdict rests on it.
- **Secondary: `qwen3:14b`** — reported, to show whether capability is what limits the result.
### Data — no new labels
The model is never fitted to labels; the only fitted quantity is θ.
- **θ is chosen on development (40 clusters)** only, with the objective used for G2 and G3: most
clusters labelled ≥ 1 accepted, subject to precision (≥ 1) ≥ 0.85; ties on more label-2 accepted,
then the higher θ. Grid: θ ∈ {0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}.
- **Primary evaluation: the G2 and G3 validation sets, 148 clusters**, which tuned neither G2 nor G4,
so G2 can be compared fairly on them. **Secondary:** test and recovery, 62 clusters.
- Identical member sets appearing in more than one file are judged once.
- **Pilot, allowed once:** the first 5 development clusters, to check the JSON parses. If more than 1
of 5 fails to parse, one prompt revision is allowed and recorded here before any other call.
### Success, on the 148-cluster primary set, with the 3B model
1. Precision (≥ 1) at least **0.80**.
2. **More** clusters labelled ≥ 1 accepted than G2 accepts on the same set.
3. Among accepted clusters of ≤ 9 members, precision (≥ 1) at least 0.67, if there are at least 3.
Also reported: G4 AND G2; FIT distribution by label; the situations named for accepted clusters
against the label names; and the 14B results against the same bar.
**Pilot (21:23):** 5 of 5 replies parsed with the 3B model, so the prompt is unchanged. About 1.6 s
per cluster. Noted without drawing a conclusion from five: the situations named are often generic
("Confrontation and hidden motives" for D04, a label-2 spouse-against-spouse cluster, FIT 0.47), and
a label-1 argument cluster scored FIT 0.87.
### Results, `qwen2.5:3b-instruct` (21:24–21:29, 245 distinct clusters in 286 s, 1 parse failure)
Log `probe/evaluate-judge-3b.log`. θ chosen on development: **0.6** (9 accepted, P1+ 0.89).
| Primary set, 148 clusters (67 ≥ 1) | Accepted | ≥ 1 | Label 2 | P1+ | Recall |
| --- | --- | --- | --- | --- | --- |
| Accept all | 148 | 67 | 10 | 0.45 | 1.00 |
| **G2** | **49** | **36** | 5 | **0.73** | **0.54** |
| G4, FIT ≥ 0.6 | 50 | 24 | 4 | 0.48 | 0.36 |
| G4 AND G2 | 18 | 11 | 1 | 0.61 | 0.16 |
On the secondary set (62) G4 reaches P1+ 0.71 against G2's 0.82. **All three criteria fail**
(P1+ 0.48; 24 ≥ 1 against G2's 36; 13 accepted small clusters at P1+ 0.23).
**FIT does not separate the labels:** mean 0.39 for label 0, 0.49 for label 1 — and **0.42 for label
2**, lower than for label 1. Precision at the primary set (0.48) is barely above accepting everything
(0.45).
**Why, visible in the replies:** the 3B model names situations at a level of abstraction where almost
anything fits — "confrontation", "power dynamics in social hierarchies", "tension between parties",
"coercion and manipulation" — and then lists most summaries as fitting it. A 5-member cluster labelled
0 scored FIT 1.00 as "power struggle/conflict". When it is specific it is often right ("marital
conflict" for a spouse-against-spouse cluster, "con or scam", "argue over payment"), but specificity
is not what FIT rewards. The rule against shared place, mood and profession was followed; a rule
against *abstraction* was never written, and the model supplies abstraction freely.
### 14B pilot and the one allowed revision
The `qwen3:14b` pilot failed to parse **5 of 5**: every reply's content was empty. The model spent its
whole 200-token budget in a separate thinking channel (Ollama returns it outside `content`), and the
`/no_think` suffix did not stop it. **Revision, the one allowed by the pilot rule, affecting only the
14B run:** requests set Ollama's `think: false` and allow 300 output tokens. The prompt text,
sampling, θ procedure and success criteria are unchanged. The five empty results were deleted before
re-running the pilot.
Re-run pilot: **5 of 5 parsed**, about 2.4 s per cluster. The situations named are more specific than
the 3B model's ("Wealthy person pressures someone into marriage", "Struggling artist facing societal or
personal challenges"), though an abstract one still scored FIT 0.80 ("conflict between individuals with
differing perspectives").
### Results, `qwen3:14b` (21:31–21:45, 245 clusters in 832 s, 0 parse failures)
Log `probe/evaluate-judge-14b.log`. θ chosen on development: **0.6** (8 accepted, P1+ 0.88).
| Primary set, 148 clusters (67 ≥ 1) | Accepted | ≥ 1 | Label 2 | P1+ | Recall |
| --- | --- | --- | --- | --- | --- |
| Accept all | 148 | 67 | 10 | 0.45 | 1.00 |
| **G2** | **49** | **36** | 5 | **0.73** | **0.54** |
| G4 (14B), FIT ≥ 0.6 | 42 | 22 | 4 | 0.52 | 0.33 |
| G4 (14B) AND G2 | 10 | 10 | 1 | 1.00 | 0.15 |
By size, G4 (14B) on the primary set: ≤ 9 members 17 accepted, P1+ 0.29; 10–25, 14 accepted, P1+ 0.50;
≥ 26, 11 accepted, **P1+ 0.91**. Secondary set (62): G4 P1+ 0.60 against G2's 0.82.
**All three criteria fail with the 14B model as well** (P1+ 0.52; 22 ≥ 1 against G2's 36; 17 small
clusters accepted at P1+ 0.29).
FIT now rises with the label — mean 0.40, 0.47, 0.52 for labels 0, 1, 2 — where the 3B model's did
not, so capability helps. But the separation is small next to the spread, and the 14B model still
names abstractions that fit almost anything ("Someone wants something from someone else", "Power
dynamics between individuals", "Someone holds power over someone else"), most often for small
clusters.
GPU host during both runs: no faults logged; peak 284.9 W (1 s), mean 163 W under load, 65 °C, PCIe
Gen 3 x4 throughout. Logs in `probe/gpu-logs/*2122*`.
### Verdict on G4
**Neither model's judgement is a better gate than G2**, and the pre-registered verdict (on the 3B
model) is a fail. The failure is specific and repeatable: asked for "the single situation most
summaries share", a model answers at whatever level of abstraction makes the most summaries fit, and
FIT rewards exactly that. Small clusters make it worse, because with 5–9 summaries an abstraction that
covers four or five of them is always available.
Two observations, recorded as observations because neither was a pre-registered criterion:
- **G4 (14B) AND G2 accepted 10 clusters on the primary set, and all 10 are labelled ≥ 1.** The two
gates fail in different ways — G2 admits cohesive clusters that are not situations, the model admits
abstract "situations" that are not cohesive — and their intersection is precise. It is also
narrow: recall 0.15.
- **For clusters of 26 or more, the 14B model's precision is 0.91.** The abstraction problem is a
small-cluster problem: 15 shown summaries rarely all fit one vague phrase *and* get listed.
What would test the obvious next step, none of it done: a prompt that asks for the situation **in
terms of roles and a concrete want** and rejects answers without both; scoring the *specificity* of
the named situation (for example, requiring the named situation to be closer to the member summaries
than to the corpus average in embedding space); and G2 AND G4 evaluated as a pre-registered gate on a
fresh partition, since every labelled cluster now informs this observation.
---
## Predicaments (recorded 2026-09-15, before any model call)
### What the first real review found
The designer opened `REVIEW.html` for run k60-s24 and read four groups closely:
- **Group 1 is artists.** It holds at least three different situations: an artist in a new place using
the art to get on, being paid for the work against what the work is worth, and plain money trouble.
- **Group 2 is police and court officers.** Inside it: being coerced, with the police watching or
arriving; and a crime that is stopped or found out.
- **Group 6 is flirting and infatuation.**
- **Extra group 1 is room and board**: hotels, dinners and the like.
None of these is "one person wanting something specific from another". Checked across all 20 groups,
most are held together by who is in the scenes or where they happen: *artist* in 44% of group 1's
scenes, *policeman* in 54% of group 3's, *uncle* and *lawyer* in group 4, *marriage* and *wedding* in
group 8, *restaurant* and *customers* in group 12, *hotel* in 41% of extra group 1.
**Why every earlier test missed it.** A topic group is as cohesive and as spread across stories as a
situation group, so cohesion, story spread, word dominance and the model's own judgement all passed it.
Summaries keep jobs and places, and the embeddings group on them. It took a person reading the groups.
### Decisions (designer, 2026-09-15)
1. **The unit is a predicament**, not a two-person transaction: what the main person is up against and
what they want out of it, as The Ladder's existing events are (the rent bouncing, the mail cart).
The sub-themes the designer found inside groups 1 and 2 are this kind of thing.
2. **Re-describe every scene as a predicament, then regroup**, rather than splitting topic groups by hand.
### Method, fixed before any call
- `probe/redescribe.py` gives the model each scene's own prose (not its summary) and the prompt in that
script: one sentence, at most 20 words, no names, jobs, places, businesses or story-specific objects,
saying what the person is up against and what they want. No example of the output vocabulary.
- Names are then removed deterministically (`strip_names.py`), the sentences embedded (`embed.py`), and
the embeddings clustered at k = 60.
- **Pilot first:** the first 40 scenes, with both `qwen2.5:3b-instruct` and `qwen3:14b`, read by the
designer side by side before any full run. The full run uses the model the designer prefers.
### How it is judged
- **Topic share** (`probe/topic_share.py`): for each candidate group, the share of its scenes whose
original summary contains the group's most common job, relationship or place word; median over groups
and seeds 11–13 at k = 60. The baseline on the summary embeddings is measured before any call and
recorded below. **Pass: the median falls to half the baseline or less**, with at least 30 candidate
groups remaining at k = 60 (enough material for a two-stage pack).
- **Leakage:** how many predicament sentences still contain a job, relationship or place word. Reported,
not gated.
- **The real test is the designer reading the regrouped top groups** in the review page: most should
read as one predicament. Numbers alone passed topic groups before.
### Baseline (measured 2026-09-15, before any model call)
A first measurement counted words for a person with no role at all ("man", "woman", "girl"), and scored
one group 90% topic-bound for saying "man". Those words (`GENERIC_PEOPLE` in `probe/topic_words.py`)
were taken out of the measure, and the baseline re-measured, still before any predicament was generated.
`python3 topic_share.py summary_embeddings.json` (`probe/topic-share-baseline.log`):
| Seed | Candidate groups | Median top topic-word share |
| --- | --- | --- |
| 11 | 40 | 16% |
| 12 | 41 | 20% |
| 13 | 42 | 22% |
| **Median** | **41** | **20%** |
Most topic-bound at seed 11: *wife* 47% of 15, *dinner* 47% of 15, *policeman* 40% of 15 and 38% of 16,
*brother* 38% of 8, *artist* 36% of 11. **Pass mark for the predicament groups: median 10% or less, with
at least 30 candidate groups.**
### Pilot (2026-09-15)
First 40 scenes, one request at a time on the GPU host (3B 39 s, 14B 111 s; no GPU faults; logs in
`probe/gpu-logs/*-2026-09-15-1803*`). Read side by side in `probe/pilot-compare.html` (`pilot_page.py`).
| | qwen2.5:3b-instruct | qwen3:14b |
| --- | --- | --- |
| Has a job, relationship or place word | 15 of 40 | 13 of 40 (mostly *wife*, *husband*) |
| Mean words (limit 20) | 14 | 25; 34 of 40 over |
| Most common opening | "The Kid seeks…" ×3 | "A man struggles…" ×6 |
3B mostly retells the plot, keeping names, objects and places. 14B writes something close to a
predicament but ignores the length limit and repeats a few openings. **Designer's read: scenes 1–10,
14B dramatically better in every one; stopped there. The full run uses qwen3:14b.**
Found while reading: `split.py` skips a title written in quotation marks, so three stories were
merged into the story before them. "Little Speck in Garnered Fruit" is inside "Dougherty's
Eye-Opener", "The Guilty Party" inside "A Harlem Tragedy", and "What You Want" inside "The Duel". The
corpus has 97 stories, not 94. Separately, `strip_names.py` treats *York*, *Manhattan* and *Broadway*
as personal names, which gives "New someone" in 29 of 838 summaries.
### Corrections before the full run (2026-09-15, designer's go-ahead)
The v1 data and every G1–G4 record stay as they were, in `probe/`. The corrected corpus is built in
`probe/v2/`, running the same scripts from that directory.
- **Split.** `split.py` accepts a quoted title and matches a contents entry by the part before a dash
(“THE GUILTY PARTY”—AN EAST SIDE TRAGEDY). Result: **97 stories, 839 scenes**. Exactly the three
missing stories were added, nothing was lost, and only their three host stories changed.
- **Summaries carried over.** 829 of the 839 scenes have text identical to a v1 scene and keep that
v1 summary verbatim (`carry_summaries.py`). The other 10 are summarised with the v1 model and prompt.
- **Name stripper.** New York places (New York, Manhattan, Broadway, Harlem, the Bowery, and a few
others) become "the city" before names are looked for, and days and holidays are never names. Months
stay ambiguous ("May" is a person in scene 5) and are left alone.
- **The baseline is re-measured on v2** (the v2 summaries, re-embedded), because both fixes change
what is embedded. It is recorded below before any predicament is read.
- **No trimming to 20 words.** Tried on the 14B pilot: cutting at the last clause break within 20
words removed what the person is up against in most cut sentences ("…with a promise of future money"
without "but his dishonesty sparks a violent confrontation"), and 10 of 40 had no clean break.
The full 14B sentence is embedded.
### Result (2026-09-15)
Full run: 839 scenes re-described by `qwen3:14b`, one request at a time, 37 minutes on the GPU host
(peak 270 W, mean 136 W, 65 °C, no faults; logs in `probe/gpu-logs/*-2026-09-15-1831*`). Sentences
average 25 words. 219 of 839 still contain a job, relationship or place word, most often a relationship
(*wife*, *husband*) — reported, not gated.
`topic_share.py`, k = 60, seeds 11–13, both measured on the v2 corpus:
| | Candidate groups | Median top topic-word share |
| --- | --- | --- |
| v2 summaries (baseline, `topic-share-baseline-v2.log`) | 45 | **20%** |
| Predicaments (`topic-share-predicaments.log`) | 42 | **13%** |
**The pre-registered pass mark is not met.** It asked for half the baseline or less (10%); the measure
fell by about a third, from 20% to 13%. The group-count mark is met (42 ≥ 30). The most topic-bound
groups are also much weaker than the baseline's: the worst predicament group is *room* at 40% of 5
scenes, against the baseline's *bride* at 62% of 8 and *policeman* at 60% of 20.
Not a pass and not a wash, so the decision goes to the reading test that this whole step exists for:
`groups_page.py` builds `v2/groups-predicaments-k60-s24.html`, which lists each group's predicament
sentences with each scene's original summary beneath, most cohesive group first, and asks the designer
to mark each group **one predicament**, **two or three mixed**, or **not a predicament**. The openings
repeat ("A man must" 45 times, "A man struggles" 37), so the risk to watch for is a group held together
by that phrasing rather than by a predicament.
+220
View File
@@ -0,0 +1,220 @@
# Aesop reference set: *Three Hundred Aesop's Fables* (tr. George Fyler Townsend, 1867)
A hand-made comparison baseline for the story-to-pack pipeline, a sibling of the Jacobs set in
`../jacobs/` and the Lawson set in `../lawson/`. Claude read all 313 fables and wrote every summary,
predicament, group, option and measurement row. **No inference of any kind was used.** Do not treat
it as ground truth: it is one careful reading.
Aesop has two jobs here. It is a normal reference set. It is also a test of a different claim: a story
supplies predicaments well but rarely says what a move leads to, while a fable puts situation, move
and outcome in one paragraph. The second job is measured in
[Fables as an option source](#fables-as-an-option-source).
| File | What it is |
| --- | --- |
| `corpus/three-hundred-aesops-fables.txt` | Project Gutenberg eBook #21 (public domain) |
| `split_gutenberg.py` | → `stories.json` and `chunks.json`: **313 fables, one story and one scene per fable** (see below) |
| `claude/batches/sNNN.json` | `{chunk, story, summary, predicament, moral}`, one file per run of fables, named by the first fable in it so that the `s*.json` glob sorts in fable order |
| `claude/groups_src.py` | the groups, their options, and the protagonist's standing in every fable |
| `claude/options_src.py` | the options measurement, with the definitions of its `new:` entries and the rules for `stated` |
| `claude/build.py` | checks everything, then writes `claude/records.json`, `claude/groups.json` and `claude/options_by_entry.json` |
| `claude/page.py` | → `claude/REFERENCE.html`, the readable page, with the measurement at the end |
Rebuild: `python3 split_gutenberg.py` from this directory, then
`cd claude && python3 build.py && python3 page.py`.
**One scene per fable** departs from the usual 80-word scene rule on purpose. The median fable is 97
words, and many are under 80, so the rule would discard them, and a fable split in two would lose its
turn. A group's fable count is therefore also its story count.
## What is in it
- **313 predicaments**: one sentence each (median 21 words, min 15, max 24), transplanted to a
workplace. They contain no animal, plant or god names and no story-only objects. Human role words
stay (someone powerful, a partner, a newcomer, the boss). `build.py` enforces this with a list of
every creature, plant, god and prop in the book, not with `topic_words`. The list caught nine
predicaments that used an animal word as a verb or an idiom ("crow about it", "swallow it whole",
"the lion's share"), and all nine were reworded.
- **115 stated morals**, recorded verbatim. A moral here is the fable's unquoted, generalising
sentence ("Look before you leap."). Where the lesson is only spoken by a character inside quotation
marks, as in *The Lion and the Mouse*, `moral` is `null`. `build.py` checks every moral is in the
text.
- **16 groups of 13 or more fables**, the recurrence rule scaled to this corpus (at least
max(3, 4% of 313) = 13). They range from 13 to 18 fables. **4 small groups of 5 to 12** are marked
`small` and reported separately: G17 letting a known predator inside (6), G18 the lion's share (6),
G19 a bad record when you need trust (5), G20 a warning before the trouble (6). Each fable sits in at
most one group: **253 are assigned and 60 fit nothing that recurs**.
- **Options**: 3 to 5 per group, 94 in all. 87 come from what someone in a member fable did or
refused to do (`scene:N`), 7 fill gaps, and none are archetypes. No option cites a fable outside
its own group, unlike Jacobs (8) and Lawson (14). A fable is a whole turn, so the move and the fable
are always the same scene.
- **Moves**: spine paths with a direction only (`+`, `-`, `risk`, `set`). There are no magnitudes.
**Skills** proposed for this corpus: `wit`, for seeing through a trick and turning it, and
`prudence`, for looking before you leap, knowing your limits and keeping what you have. Between them
they are most of Aesop's morals.
- **Stage** comes from protagonist standing, judged **per fable** in `STANDING` in `groups_src.py`:
157 fables rank low, 119 middle and 37 high. **No group comes out stage `high`** (11 low, 9 middle).
Aesop's "you" is usually the lamb, not the wolf, and the high-standing fables (a father, a master, a
captor) scatter across groups rather than recurring together.
The groups:
| | Group | Fables | Nearest catalogue entry |
| --- | --- | --- | --- |
| G01 | A friendly approach from someone who would gain by your fall | 16 | new (near `sure_thing`) |
| G02 | At the mercy of someone who has already decided | 14 | `at_their_mercy` |
| G03 | Trading one hardship for another | 14 | new |
| G04 | Envying another's gift | 17 | new (near `overlooked`) |
| G05 | Passing for more than you are | 14 | `pretence` |
| G06 | Riding high | 13 | new |
| G07 | Sneered at by someone showier | 13 | new (near `power_over_you`) |
| G08 | Help asked in need | 14 | `plea_for_help` |
| G09 | Wanting more than you hold | 13 | new |
| G10 | Your side divided | 15 | new |
| G11 | A partner you may not be able to count on | 18 | new (near `used`) |
| G12 | Rushing in without looking | 13 | new |
| G13 | Pointing at others' faults | 14 | new (near `own_words_back`) |
| G14 | Liberties between the weak and the strong | 15 | new (near `revenge`) |
| G15 | A hard lot | 14 | new (near `dead_end`) |
| G16 | Worked hard, badly repaid | 13 | new (near `overlooked`) |
Only three large groups match an existing entry. The rest are situations about **self-regard and
prudence** (envy, pride, greed, heedlessness, pointing at faults), which no story corpus isolated.
The story corpora found them only as the texture of longer plots.
## How this corpus differs
**Recurrence is the corpus's nature, not something to be found.** Every earlier corpus needed a
search for the handful of situations its stories kept returning to. Aesop is built from a small
stock of situations told over and over with different animals: the predator's invitation alone
appears sixteen times. The problem flips. Nearly every fable recurs, and the work is deciding which
of several overlapping situations a fable belongs to. *The Fox and the Goat* is a trap, a heedless
leap, and a partner who uses you. It sits in G01.
**The predicaments are the cleanest yet.** There are no names to strip, and the whole cast is
already a set of roles: the strong, the weak, the flatterer, the partner, the master. Transplanting
to a workplace needs only the animal removed. The cost is abstraction: a pack built on these
predicaments alone would have no setting at all.
**Two groups hold both sides of one situation.** G13 holds the critic who shares the fault and the
one criticised. G14 holds the weak who strike at the fallen and the strong being needled. In Jacobs
and Lawson the actor was always on one side. Here the fable often follows both parties to their fate,
and splitting them would leave neither side at 13. The measurement does the same for `sneered_at`:
G07's predicaments are all from the side of the one sneered at, but four of its fables (the heifer, the
horse, the olive, the hare) also supply the sneerer's move as an option.
**Power runs through nearly everything.** Five of the sixteen groups turn on an imbalance of power
(G01, G02, G03, G14, G16), and two of the small ones as well. Of the options measured below, 110 of
242 are about the actor's standing going down. It is the most power-heavy corpus so far, and the
least domestic: there is almost no home, love, grief or dread.
## Fables as an option source
`claude/options_by_entry.json` maps a catalogue entry (a v2 id, or `new:<id>` defined under
`_new_entries`) to the options the fables supply for it. Each option is
`{text, moves, source: "chunk:N", evidence, stated}`. `evidence` is the verbatim moral, or an
excerpt of at most 30 words, and `build.py` checks both. **`stated` is `true` only if the fable's own
outcome or moral shows the direction of every path in `moves`.** The rules, applied the same way to
every row, are in the docstring of `options_src.py`. The main points:
- An outcome is read as a direction by a fixed table. Killed, caught, beaten, driven out or publicly
rebuked counts as `standing -`. Goods, food or pay lost counts as `money -`. A counterpart who
repays, deserts or stops trusting you counts as `relationships`. Reading "eaten" as `standing -` is
a convention. The fable does not say it.
- A moral that only calls a move wise or foolish ("Look before you leap.") states no direction. One
that names the effect ("Slow but steady wins the race.") does.
- An outcome the move did not cause is not counted, however pointed. The sparrow is taken by a hawk
while mocking the hare, but mocking did not bring the hawk.
- `risk` is stated only when one fable shows the same move ending both ways. That happens once, in
*The Birdcatcher, the Partridge, and the Cock*: one bird's plea spares it, and the other's does not.
- **Measurement moves use only `money`, `standing` and `relationships.<role>`.** A skill label or a
flag is a decision about the option, not an outcome any text can show. Leaving them out means every
row could in principle be stated. The group options in `groups_src.py` keep them.
**Results:**
| | |
| --- | --- |
| Catalogue v2 entries with at least one sourced option | **32 of 82** (26 with at least one stated option) |
| `new:` entries | **19**, all with at least one stated option |
| Options | **242**, from 229 fables (84 for v2 entries, 158 for new ones) |
| `stated: true` | **165 (68%)**: 62 of 84 for v2 entries (74%), 103 of 158 for new ones (65%) |
| Entries with three or more stated options | 21 |
| Entries with a stated `+` option and a stated `-` option | 15 |
The v2 entries the fables speak to are mostly about power, deception and help. `at_their_mercy`
gets 14 options (12 stated), `plea_for_help` 11 (9), `pretence` 8 (6) and `warning` 5 (2). Another 28
entries get one to four options each. The 50 entries with nothing are almost all of the catalogue's
home, love, loss, fear and duty. Examples are `home_unseen`, `grief`, `losing_them`, `dread`,
`duty_against_love`, `past_comes_back` and `promised_elsewhere`. Aesop has no inner life and almost
no family.
The 19 new entries are `friendly_predator`, `better_the_devil`, `ape_your_betters`, `riding_high`,
`sneered_at`, `grasping`, `divided`, `doubtful_ally`, `heedless`, `pot_and_kettle`,
`needling_the_strong`, `hard_lot`, `used_up`, `predator_inside`, `lions_share`, `bad_record`,
`asking_above`, `please_everyone` and `make_them_move`. Each definition says which v2 entry it sits
nearest to and how the decision differs.
**Against the story packs.** Every option in a story pack has an inferred direction: the author
judged where the move leads, because the story does not say.
| Pack | Options | `scene:N` | `archetype` | `filled` | Directions stated by the text |
| --- | --- | --- | --- | --- | --- |
| Lawson (`../lawson/claude/groups.json`) | 142 | 118 | 12 | 12 | 0 (all inferred) |
| Bierce (`../bierce/claude/groups.json`) | 128 | 96 | 15 | 17 | 0 (all inferred) |
| Aesop groups (`claude/groups.json`) | 94 | 87 | 0 | 7 | not separately judged |
| Aesop measurement (`claude/options_by_entry.json`) | 242 | 242 (all from a fable) | 0 | 0 | 165 (68%) |
Even a `scene:N` option in a story pack is sourced only for its *move*: the scene shows someone doing
it, and its effect is the author's call. In the measurement, two thirds of the options are sourced for
the effect as well.
The 68% needs three qualifications before anyone relies on it.
1. **The stated options are thin.** 226 of the 242 options carry a single path, and 15 of the 16
two-path options are stated. A fable follows one actor to one fate, and that fate is nearly always
their standing. Story-pack options carry more: Lawson's average 1.67 spine paths each, Bierce's
1.18 and Aesop's own group options 1.40, before skills and flags. What a fable usually leaves
unsaid is the second path, how the move changes a relationship.
2. **They are almost all downside.** Of the 165 stated options, **136 move only in the `-`
direction**. By path, 90 stated `standing -` against 12 stated `standing +`. A fable states the
price of the tempting mistake: the goat in the well, the crow who sang, the dog who dropped his
meat. The wise move's reward is usually just that the harm does not happen, and that gives no
direction. The fox who stays outside the sick lion's cave is right, and nothing moves for him. That
is why the options marked `false` are so often the prudent ones. Only 15 entries get a stated
option in each direction.
3. **The coverage is narrow.** 32 of 82 entries, with most of the weight on a handful. The fables
tell you a great deal about predators, pretenders and the greedy, and nothing about a spouse, a
dead friend or a night watch.
**Verdict.** Fables are a real source of effect directions, and a far better one than stories, but
only for one kind of option. For a tempting mistake in a situation of power, greed, pride or trust,
the fable states where it leads, with evidence, seven times in ten (136 of the 190 options that only
move down). For any other option it is about half (29 of 52). That is enough to anchor
the `-` direction of an event's tempting option, and to check an author's inferred direction against
a text. Fables do not supply the prudent option's payoff, the relationship side of a move, or skills,
and they say almost nothing about the catalogue's domestic and inward half. The same applies to them as
a source of predicaments. Use them to calibrate the downside of options in power and trust events,
not to build whole events.
## Known defects
- **Drop capitals.** The transcription lost each fable's printed drop capital, so many fables open
with the first word's initial missing: "WOLF, meeting with a Lamb" (fable 1), "TEE KITES" (180).
`chunks.json` is left as split. Morals and evidence are quoted from later in the text, so none is
affected.
- **Other transcription slips**, left as split: "an a woman" (194), "use I gave credence" (280),
"all bag in front" (260).
- **Fable 89, *The Æthiop***, rests on a racist premise, that a black servant's colour is dirt to be
scrubbed off. Its predicament keeps only the transplantable turn: trying to scrub away someone's
nature, and hurting them. It is unassigned.
- **Etiologies are not situations.** *The Goods and the Ills* (250), *The Lark Burying Her Father*
(263), *The Man, the Horse, the Ox, and the Dog* (203) and *The Two Bags* (260) explain how
something came to be. Their predicaments are strained, and three of the four are unassigned.
- **G11 is the loosest large group** (18 fables). It holds every doubtful partner, from the companion
who flees a bear to the boastful suitor, joined only by the question "can I count on them?".
- **Standing is judged per fable** from the predicament's "you", not from the animal. So the lion in
*The Lion and the Mouse* ranks high because the predicament puts the player in the lion's place.
The Lorimer advice-book pilot running alongside this one was not read. The two results are
independent.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
[
{"chunk": 0, "story": 0, "summary": "A lion spares a mouse who begs for its life, and later the mouse gnaws through the hunters' ropes that bind the lion.", "predicament": "Someone far beneath you whom you could crush begs to be let off and swears they will repay you one day.", "moral": null},
{"chunk": 1, "story": 1, "summary": "A wolf invents one charge after another against a lamb, and when each is refuted eats him anyway.", "predicament": "Someone powerful who has already decided to ruin you keeps inventing charges, and each answer you give changes nothing.", "moral": "The tyrant will always find a pretext for his tyranny."},
{"chunk": 2, "story": 2, "summary": "An ass envies the grasshoppers' song, tries to live on dew as they claim to, and starves.", "predicament": "You admire a colleague's talent so much that you copy their habits, though what feeds them will not feed you.", "moral": null},
{"chunk": 3, "story": 3, "summary": "A crane pulls a bone from a wolf's throat and, asking for her fee, is told that keeping her head is payment enough.", "predicament": "You did risky work for someone ruthless on a promise of payment, and now they say escaping unharmed is your fee.", "moral": "In serving the wicked, expect no reward, and be thankful if you escape injury for your pains."},
{"chunk": 4, "story": 4, "summary": "A father shows his quarrelling sons that a bundle of sticks cannot be broken though each stick alone snaps easily.", "predicament": "The people under you quarrel constantly among themselves, and you must make them see that divided they are easy prey.", "moral": null},
{"chunk": 5, "story": 5, "summary": "A bat caught by two weasels claims to be a mouse to one and a bird to the other, and escapes both.", "predicament": "Two hostile powers each catch you in turn, and you could save yourself by claiming to be whatever each one tolerates.", "moral": "It is wise to turn circumstances to good account."},
{"chunk": 6, "story": 6, "summary": "A cock scratching for food finds a jewel and says one grain of barley would be worth more to him.", "predicament": "Something others would prize highly has fallen into your hands, but it is of no use to you in your present need.", "moral": null},
{"chunk": 7, "story": 7, "summary": "A swallow and a crow argue over plumage, and the crow says his feathers at least last through the winter.", "predicament": "A showy colleague belittles you, and you must decide whether to argue looks or point to what lasts when times turn hard.", "moral": "Fair weather friends are not worth much."},
{"chunk": 8, "story": 8, "summary": "A just lion king proclaims peace between strong and weak beasts, and the hare, praising it, runs for his life.", "predicament": "A new leader declares that the weak will now stand safely beside the strong, and you are one of the weak.", "moral": null},
{"chunk": 9, "story": 9, "summary": "A traveller scolds his dog for dawdling, and the dog replies that he is ready and waiting on his master.", "predicament": "Someone above you blames you for the delay, when in fact you have long been ready and are waiting on them.", "moral": "The loiterer often blames delay on his more active friend."},
{"chunk": 10, "story": 10, "summary": "A starving grasshopper who sang all summer begs food of the ants, who mock him for not saving.", "predicament": "Someone who idled while you worked and saved now comes to you hungry, asking you to share what you stored.", "moral": null},
{"chunk": 11, "story": 11, "summary": "A hare mocks a tortoise's pace, sleeps during their race, and loses to her steady plodding.", "predicament": "Someone gifted mocks your slowness and accepts your challenge, sure they can win without really trying.", "moral": "Slow but steady wins the race."},
{"chunk": 12, "story": 12, "summary": "A charcoal-burner invites a fuller to share his house to save costs, and the fuller refuses because he would blacken all his work.", "predicament": "A friend proposes sharing premises to save money, but the way they work would undo everything you do.", "moral": "Like will draw like."},
{"chunk": 13, "story": 13, "summary": "A boy catching locusts reaches for a scorpion, which warns him he would have lost everything had he touched it.", "predicament": "You have been collecting easy wins, and the next thing you are about to grab looks like the others but is dangerous.", "moral": null},
{"chunk": 14, "story": 14, "summary": "A fisherman pipes to the fish in vain, then casts his net and scolds them for dancing only now.", "predicament": "You have waited in vain for people to come round of their own accord, and could stop coaxing and use the tool you have.", "moral": null},
{"chunk": 15, "story": 15, "summary": "A dog carrying meat snaps at his reflection's larger piece and loses his own.", "predicament": "You already hold something good, and a bigger prize that may not be real tempts you to let go of it.", "moral": null},
{"chunk": 16, "story": 16, "summary": "A carter whose wagon is stuck calls on Hercules, who tells him to put his shoulder to the wheel first.", "predicament": "Your work is stuck, and you are waiting for someone powerful to rescue you instead of putting your own effort into it.", "moral": "Self-help is the best help."},
{"chunk": 17, "story": 17, "summary": "A blind young mole insists he can see, and his mother's test shows he cannot even smell.", "predicament": "A young person insists they have a skill they plainly lack, and you must decide how to show them without crushing them.", "moral": null},
{"chunk": 18, "story": 18, "summary": "A herdsman vows an offering to find his calf's thief, then finds a lion eating it and vows more to escape.", "predicament": "You swore to track down whoever took from you, and have just found out the culprit is far more dangerous than you.", "moral": null},
{"chunk": 19, "story": 19, "summary": "A fawn asks why his mother, bigger and armed, flees from hounds, and she admits she simply cannot help it.", "predicament": "You have every advantage over the one who frightens you, and still you cannot bring yourself to stand your ground.", "moral": "No arguments will give courage to the coward."},
{"chunk": 20, "story": 20, "summary": "A fox betrays his partner the ass to a lion in return for his own safety, and the lion takes them both.", "predicament": "Danger threatens you and a partner, and someone powerful offers to spare you if you hand your partner over.", "moral": null},
{"chunk": 21, "story": 21, "summary": "Flies gorge on spilt honey, stick fast in it and die lamenting their greed.", "predicament": "An easy pleasure is spread out in front of you, and the longer you indulge the harder it will be to get free.", "moral": "Pleasure bought with pains, hurts."},
{"chunk": 22, "story": 22, "summary": "The beasts ask the lioness how many young she bears, and she says only one, but that one a lion.", "predicament": "Others boast of how much they produce and demand to know your numbers, when you produce little but of real quality.", "moral": "The value is in the worth, not in the number."},
{"chunk": 23, "story": 23, "summary": "A farmer warms a frozen snake in his bosom, and it revives and bites him fatally.", "predicament": "Someone known for treachery is down and helpless, and you could take them in and nurse them back to strength.", "moral": "The greatest kindness will not bind the ungrateful."},
{"chunk": 24, "story": 24, "summary": "A man points to a statue of a man strangling a lion, and the lion says a lion would have carved it the other way.", "predicament": "In a dispute over who is stronger, your rival points to evidence that was produced entirely by their own side.", "moral": "One story is good, till another is told."},
{"chunk": 25, "story": 25, "summary": "A bramble tells a pomegranate and an apple tree to stop disputing which is more beautiful in its presence.", "predicament": "Two respected colleagues argue over who is the best, and someone who counts for nothing loudly sets themselves up as judge.", "moral": null},
{"chunk": 26, "story": 26, "summary": "A stork caught in a net with thieving cranes pleads his good character, but the farmer kills him with them.", "predicament": "You are caught alongside people who were doing wrong, and your good character counts for nothing because of the company you kept.", "moral": "Birds of a feather flock together."},
{"chunk": 27, "story": 27, "summary": "A mountain groans as if in labour, crowds gather in dread, and out comes a mouse.", "predicament": "Someone has made a huge commotion about what they are about to produce, and everyone is gathered waiting for the result.", "moral": "Don’t make much ado about nothing."},
{"chunk": 28, "story": 28, "summary": "A bear boasts he loves men so much he never touches their dead, and a fox wishes he spared the living instead.", "predicament": "Someone boasts of their kindness in a way that costs them nothing, while they keep harming those who are still around.", "moral": null},
{"chunk": 29, "story": 29, "summary": "A tortoise begs an eagle to teach her to fly, promising riches, and is dropped and dashed to pieces.", "predicament": "You long for a role quite beyond your nature, and someone offers to lift you into it if you pay well.", "moral": "If men had all they wished, they would be often ruined."},
{"chunk": 30, "story": 30, "summary": "A fox trapped in a well praises the water to a goat, climbs out on the goat's back, and leaves him there.", "predicament": "Someone already in trouble urges you to join them in a venture, praising it highly, and you have not checked the way out.", "moral": "Look before you leap."},
{"chunk": 31, "story": 31, "summary": "A raven envies a swan's whiteness, moves to the lakes to wash, and starves without changing colour.", "predicament": "You envy a colleague's standing and give up the work that feeds you to copy their habits, hoping to become like them.", "moral": "Change of habit cannot alter Nature."},
{"chunk": 32, "story": 32, "summary": "A thirsty pigeon flies at a painted goblet on a signboard and breaks her wings.", "predicament": "You badly need something, and what looks like it has just appeared, so you rush at it without checking that it is real.", "moral": "Zeal should not outrun discretion."},
{"chunk": 33, "story": 33, "summary": "A dog lying in a manger snaps at the oxen to stop them eating hay he cannot eat himself.", "predicament": "Something you cannot use yourself sits under your control, and the people who need it are waiting for you to let them have it.", "moral": null},
{"chunk": 34, "story": 34, "summary": "Oxen hauling a wagon rebuke the creaking axles for complaining when it is the oxen doing the work.", "predicament": "You carry the heaviest share of the work in silence, while someone who does far less complains loudly about the strain.", "moral": "Those who suffer most cry out the least."},
{"chunk": 35, "story": 35, "summary": "Cranes learn a farmer's sling is only for show and ignore it, until he loads it with stones and kills many.", "predicament": "Someone in charge has threatened you many times without following through, and you have started to ignore the threats.", "moral": "If words suffice not, blows must follow."},
{"chunk": 36, "story": 36, "summary": "An old lion feigns sickness and eats his visitors, until the fox notices tracks going in and none coming out.", "predicament": "A powerful person asks you to come in close to talk, and you notice that others who went in never came back.", "moral": "He is wise who is warned by the misfortunes of others."},
{"chunk": 37, "story": 37, "summary": "One traveller climbs a tree and leaves his friend to face a bear, and the friend later says the bear warned him against such companions.", "predicament": "When trouble struck, your companion saved themselves and left you to face it alone, and now they joke about it.", "moral": "Misfortune tests the sincerity of friends."},
{"chunk": 38, "story": 38, "summary": "A fox who lost his tail urges all the other foxes to cut theirs off, and one sees through him.", "predicament": "Someone who has suffered a loss urges everyone to give up the same thing, claiming it is an improvement for all.", "moral": null},
{"chunk": 39, "story": 39, "summary": "A cat accuses a cock of crowing at night to justify eating him, and eats him despite his defence.", "predicament": "Someone with power over you has already decided to be rid of you and is only looking for a respectable excuse.", "moral": null},
{"chunk": 40, "story": 40, "summary": "A wolf disguised in a sheep's skin is shut in the fold and killed by the shepherd who takes him for mutton.", "predicament": "You have disguised yourself as one of the harmless crowd to get what you want, and now you are treated exactly like them.", "moral": "Harm seek, harm find."},
{"chunk": 41, "story": 41, "summary": "A goatherd breaks a stray goat's horn with a stone and begs it not to tell his master, and the goat says the horn will speak.", "predicament": "You have damaged something in your care and ask a witness to keep quiet, but the damage itself will be plain to see.", "moral": "Do not attempt to hide things which cannot be hid."},
{"chunk": 42, "story": 42, "summary": "A returned traveller boasts of a great leap made abroad, and a bystander tells him to leap it here.", "predicament": "You have been boasting of a feat done far away with witnesses, and someone asks you to do it again right here.", "moral": null},
{"chunk": 43, "story": 43, "summary": "A woodcutter agrees to let a lion court his daughter if he gives up his teeth and claws, then drives him off.", "predicament": "Someone you fear demands something you cannot refuse outright, so you set a condition that will leave them harmless.", "moral": null},
{"chunk": 44, "story": 44, "summary": "A miser's buried gold is stolen, and a neighbour tells him a stone in the hole would serve him just as well.", "predicament": "Wealth you hoarded and never used has been taken from you, and someone points out you were no poorer before.", "moral": null}
]
@@ -0,0 +1,49 @@
[
{"chunk": 45, "story": 45, "summary": "A pig squeals when the shepherd seizes him, and tells the complaining sheep and goat that they are handled for wool and milk but he for his life.", "predicament": "Colleagues who are only ever asked for small things scold you for resisting, when what is demanded of you is everything.", "moral": null},
{"chunk": 46, "story": 46, "summary": "A boy grasps so many filberts that he cannot pull his hand out of the pitcher, and is told to take half.", "predicament": "You have grabbed so much at once that you cannot get any of it out, and letting go of half feels like losing.", "moral": "Do not attempt too much at once."},
{"chunk": 47, "story": 47, "summary": "Frogs ask Jupiter for a king, scorn the log and the eel he sends, and are given a heron who eats them.", "predicament": "Your group keeps complaining that its mild leader is too weak and demanding a stronger one from above.", "moral": null},
{"chunk": 48, "story": 48, "summary": "A father who maimed the snake that killed his son offers it peace, and the snake says neither can ever forget.", "predicament": "You and someone you hurt, and who hurt you far worse, both have reason to hate, and you now offer to make peace.", "moral": "No one truly forgets injuries in the presence of him who caused the injury."},
{"chunk": 49, "story": 49, "summary": "A lion woken by a mouse searches his den in fury, and tells a mocking fox it is the mouse's cheek, not the mouse, he resents.", "predicament": "Someone far beneath you has taken a small liberty, and a bystander mocks you for making so much of it.", "moral": "Little liberties are great offenses."},
{"chunk": 50, "story": 50, "summary": "A groom curries his horse all day but sells its oats, and the horse asks to be groomed less and fed more.", "predicament": "Someone responsible for you makes a great show of looking after you while quietly skimming off what you actually need.", "moral": null},
{"chunk": 51, "story": 51, "summary": "A mule refuses to take a little of the struggling ass's load, the ass dies, and the mule must carry all of it and the ass's hide.", "predicament": "A partner struggling under their share asks you to take a little of it, and you could say it is not your job.", "moral": null},
{"chunk": 52, "story": 52, "summary": "An ass envious of the petted lapdog bursts into the house to fawn on his master and is beaten back to the stable.", "predicament": "You work hard while a charming favourite is pampered for doing little, and you are tempted to try the favourite's tricks.", "moral": null},
{"chunk": 53, "story": 53, "summary": "Oxen plot to kill the butchers, but an old ox warns that clumsier ones would replace them.", "predicament": "Your group is set on getting rid of the people who harm it, and you suspect whoever replaces them will be worse.", "moral": "Do not be in a hurry to change one evil for another."},
{"chunk": 54, "story": 54, "summary": "A shepherd boy raises false alarms of a wolf for fun, and when the wolf truly comes nobody answers his cry.", "predicament": "You have raised false alarms before to amuse yourself, and now there is a real crisis and you need everyone to believe you.", "moral": "There is no believing a liar, even when he speaks the truth."},
{"chunk": 55, "story": 55, "summary": "Boys pelting frogs for fun are told by one frog that their sport is death to the frogs.", "predicament": "People above you are amusing themselves at your expense, and what is a game to them is ruin to you.", "moral": null},
{"chunk": 56, "story": 56, "summary": "An ass who lightened his salt load by falling in a stream tries the trick with sponges and doubles his burden.", "predicament": "A shortcut that once lightened your load has been noticed, and you are tempted to try it again.", "moral": null},
{"chunk": 57, "story": 57, "summary": "A biting dog made to wear a warning bell struts proudly with it until an old hound tells him it is a mark of disgrace.", "predicament": "You have been singled out with a mark you take for an honour, and an old hand tells you it is a public warning.", "moral": "Notoriety is often mistaken for fame."},
{"chunk": 58, "story": 58, "summary": "A goatherd feeds stray wild goats better than his own to win them, and they leave, reasoning he would do the same to them.", "predicament": "Someone courting you treats you better than the people who have served them for years, and you wonder what that means for you.", "moral": "Old friends cannot with impunity be sacrificed for new ones."},
{"chunk": 59, "story": 59, "summary": "A greying man courts a young and an old woman, who between them pluck out all his hair.", "predicament": "You are courting two parties who want opposite things from you, and each is quietly remaking you to suit themselves.", "moral": "Those who seek to please everybody please nobody."},
{"chunk": 60, "story": 60, "summary": "A sick stag's many visitors each eat some of his food, so he dies of want rather than illness.", "predicament": "While you are laid low, a stream of well-wishers visits, and each helps themselves to a little of what you need.", "moral": "Evil companions bring more hurt than profit."},
{"chunk": 61, "story": 61, "summary": "A boy stung after touching a nettle gently is told by his mother to grasp it boldly next time.", "predicament": "You approached a prickly problem timidly and got hurt, and someone tells you only a bold, firm grip will work.", "moral": "Whatever you do, do with all your might."},
{"chunk": 62, "story": 62, "summary": "An astronomer gazing at the stars falls into a well, and a neighbour asks why he cannot see what is on earth.", "predicament": "You have been so absorbed in lofty plans that you have fallen into trouble right in front of you, and must call for help.", "moral": null},
{"chunk": 63, "story": 63, "summary": "Wolves persuade the sheep to send away their guard dogs for peace, and then devour the unguarded flock.", "predicament": "Your enemies offer peace if only you dismiss the people who protect you, claiming those protectors are the cause of all trouble.", "moral": null},
{"chunk": 64, "story": 64, "summary": "A cat disguised as a doctor offers to treat ailing birds, who say they will stay well if he goes away.", "predicament": "Someone known to prey on people like you arrives dressed as a helper, offering to fix your troubles if you let them in.", "moral": null},
{"chunk": 65, "story": 65, "summary": "A jackdaw decks himself in others' fallen feathers to be chosen king, and the birds pluck them back.", "predicament": "You are presenting yourself for a top post decked out in achievements borrowed from colleagues, who are about to see it.", "moral": null},
{"chunk": 66, "story": 66, "summary": "A kid on a rooftop taunts a passing wolf, who says it is the roof that mocks him.", "predicament": "You are safely out of reach of someone powerful for the moment, and it is tempting to taunt them while you can.", "moral": "Time and place often give the advantage to the weak over the strong."},
{"chunk": 67, "story": 67, "summary": "A doctor robs a blind old woman while treating her, and when he sues for his fee she says she still cannot see her goods.", "predicament": "Someone you hired has been quietly robbing you while doing the job, and now demands full payment under your agreement.", "moral": null},
{"chunk": 68, "story": 68, "summary": "A mother frog puffs herself up to match the ox that killed her son, and is told she would burst first.", "predicament": "Someone far bigger has crushed one of yours, and you are tempted to puff yourself up to look their equal.", "moral": null},
{"chunk": 69, "story": 69, "summary": "A dying farmer tells his sons treasure lies in the vineyard, and their digging brings a great harvest.", "predicament": "You want those who will take over from you to work as hard as you did, and plain advice will not move them.", "moral": null},
{"chunk": 70, "story": 70, "summary": "A heifer mocks a working ox for his labour and is then led off to be sacrificed while the ox is freed.", "predicament": "An idle favourite mocks you for how hard you work, not seeing why they have been kept free of work.", "moral": null},
{"chunk": 71, "story": 71, "summary": "A victorious cock crows on a wall and is carried off by an eagle, leaving the beaten cock to rule.", "predicament": "You have just beaten your rival, and it is tempting to boast about it from the most exposed place you can find.", "moral": "Pride goes before destruction."},
{"chunk": 72, "story": 72, "summary": "An old warhorse sent to work a mill laments his former glory, and the miller tells him fortune rises and falls.", "predicament": "Age has moved you from a proud post to drudgery, and you keep bemoaning what you had to whoever will listen.", "moral": null},
{"chunk": 73, "story": 73, "summary": "A jealous fox leads the newly elected monkey king into a trap, then mocks him as unfit to rule.", "predicament": "You have just been raised to a high post, and a jealous colleague brings you a tempting windfall they say they kept for you.", "moral": null},
{"chunk": 74, "story": 74, "summary": "A soldier pampers his horse in war, drudges it in peace, and finds it cannot carry him when war returns.", "predicament": "Someone valued you highly in a crisis, worked you into the ground when it passed, and now needs you at your best again.", "moral": null},
{"chunk": 75, "story": 75, "summary": "The limbs rebel against the idle-seeming belly and stop feeding it, and the whole body weakens.", "predicament": "You and your fellow workers resent a part of the organisation that seems to do nothing, and are tempted to stop supplying it.", "moral": null},
{"chunk": 76, "story": 76, "summary": "Maids kill the cock that wakes their mistress early, and she wakes them even earlier.", "predicament": "Your boss works you too early and too long, and you could get rid of the thing that sets the pace.", "moral": null},
{"chunk": 77, "story": 77, "summary": "A vine cropped by a goat says she will provide the wine poured over him at his sacrifice.", "predicament": "Someone is stripping you bare without cause, and you know that in time you will have a hand in their downfall.", "moral": null},
{"chunk": 78, "story": 78, "summary": "A monkey presents her ugly baby for the beauty prize and, laughed at, says he is the most beautiful to her.", "predicament": "You have put forward work of your own that everyone laughs at, and you are sure it is the best there is.", "moral": null},
{"chunk": 79, "story": 79, "summary": "Pigeons invite a hawk to protect them from a kite, and he kills more of them in a day than the kite did in a year.", "predicament": "Threatened by one predator, your group is ready to invite a powerful protector inside who may prove far worse.", "moral": "Avoid a remedy that is worse than the disease."},
{"chunk": 80, "story": 80, "summary": "A sprat offers to reconcile warring dolphins and whales, who say they would rather be destroyed than accept him.", "predicament": "Two powerful factions are at war, and you, a nobody, offer to step in and settle their quarrel.", "moral": null},
{"chunk": 81, "story": 81, "summary": "A swallow nests in the wall of a court of justice, and her young are eaten by a serpent there.", "predicament": "You trusted the one place meant to protect everyone's rights, and it is exactly there that you have been wronged.", "moral": null},
{"chunk": 82, "story": 82, "summary": "An earthen pot floating downstream begs a brass pot to keep away, since any touch would break her.", "predicament": "Someone much stronger than you wants to travel alongside you, and even friendly contact could break you.", "moral": "Equals make the best friends."},
{"chunk": 83, "story": 83, "summary": "A shepherd raises a wolf cub to steal lambs from neighbours, and it warns him to watch his own flock.", "predicament": "You have trained someone to take advantage of others for you, and they have just hinted they could do the same to you.", "moral": null},
{"chunk": 84, "story": 84, "summary": "A crab tells her son to walk straight, and he asks her to show him how.", "predicament": "You are correcting a junior for a fault you plainly share, and they ask you to demonstrate the right way.", "moral": "Example is more powerful than precept."},
{"chunk": 85, "story": 85, "summary": "A father's two daughters, married to a gardener and a tile-maker, wish for rain and for sun, and he cannot back both.", "predicament": "Two people you care for equally need opposite outcomes, and whichever you support you will be working against the other.", "moral": null},
{"chunk": 86, "story": 86, "summary": "A mother praises her son's first small thefts, and on his way to execution he bites her ear for not stopping him.", "predicament": "Someone in your charge has brought you a small ill-gotten gain, and you could praise it or stop it now.", "moral": null},
{"chunk": 87, "story": 87, "summary": "A weary old woodcutter calls for Death, and when Death comes asks only for help lifting his load.", "predicament": "Worn out, you have loudly wished to be done with it all, and now the chance to quit is actually offered.", "moral": null},
{"chunk": 88, "story": 88, "summary": "A fir scorns a useless bramble, which reminds it of the axes waiting to fell the useful.", "predicament": "Someone important sneers that you are of no use, and you know their usefulness is exactly what marks them to be cut down.", "moral": "Better poverty without care, than riches with."},
{"chunk": 89, "story": 89, "summary": "A buyer of a black servant, thinking his colour is dirt, scrubs him incessantly and only gives him a cold.", "predicament": "You have taken someone on convinced that their nature is a fault you can scrub away, and your efforts are only hurting them.", "moral": "What’s bred in the bone will stick to the flesh."},
{"chunk": 90, "story": 90, "summary": "A frog ties a mouse's foot to his own and drowns him, and a hawk carries off both.", "predicament": "A new acquaintance proposes binding your fortunes tightly together, and you would be on their ground, not yours.", "moral": "Harm hatch, harm catch."},
{"chunk": 91, "story": 91, "summary": "A fisherman's net holds the large fish, but the small ones slip back through the meshes.", "predicament": "A sweep that catches the big players lets the small ones slip away, and you are either doing the sweeping or being swept.", "moral": null}
]
@@ -0,0 +1,48 @@
[
{"chunk": 92, "story": 92, "summary": "A wounded wolf asks a passing sheep to fetch him water, and the sheep sees he would make her the meat as well.", "predicament": "Someone known to prey on people like you is laid up and asks you for a small favour that would bring you within reach.", "moral": "Hypocritical speeches are easily seen through."},
{"chunk": 93, "story": 93, "summary": "An old woman sniffs an empty wine jar and praises how fine the wine must have been.", "predicament": "Only the faint trace of someone's good work is left behind, and you find yourself still admiring what it must have been.", "moral": "The memory of a good deed lives."},
{"chunk": 94, "story": 94, "summary": "A man bitten by a dog is advised to feed the dog bread dipped in his blood, and says that would invite every dog to bite him.", "predicament": "Someone who hurt you could be soothed with a reward, and you are advised that appeasing them is the cure.", "moral": "Benefits bestowed upon the evil-disposed increase their means of injuring you."},
{"chunk": 95, "story": 95, "summary": "A huntsman and a fisherman swap their catches daily until a neighbour warns that habit will spoil the pleasure.", "predicament": "An exchange with a partner has pleased you both so much that you repeat it daily, and someone warns it will soon go stale.", "moral": "Abstain and enjoy."},
{"chunk": 96, "story": 96, "summary": "A fox flatters a crow's beauty and doubts her voice, so she caws and drops her meat to him.", "predicament": "Someone who wants what you hold praises you lavishly, then hints at one doubt you would have to prove wrong by speaking up.", "moral": null},
{"chunk": 97, "story": 97, "summary": "A poor widow shears her sheep herself to save the fee and cuts his flesh with the wool.", "predicament": "To save an expert's fee you are doing a skilled job yourself, and you are hurting the thing you meant to profit from.", "moral": "The least outlay is not always the greatest gain."},
{"chunk": 98, "story": 98, "summary": "An ass dances on the roof and is beaten, protesting that everyone laughed when the monkey did it.", "predicament": "You copied a favourite's antics that everyone applauded, and are being punished for exactly what they were praised for.", "moral": null},
{"chunk": 99, "story": 99, "summary": "A hunted stag hides in an ox-stall, escapes the servants' notice, and is found by the sharp-eyed master.", "predicament": "You are hiding from trouble in a place you should not be, the careless have passed you by, and the sharp-eyed boss is coming.", "moral": null},
{"chunk": 100, "story": 100, "summary": "A hound complains that the housedog feasts on his catch, and the housedog tells him to blame the master who taught him so.", "predicament": "You do the hard work while a colleague enjoys its rewards, and they say the fault lies with the boss who arranged it.", "moral": "Children are not to be blamed for the faults of their parents."},
{"chunk": 101, "story": 101, "summary": "A lion and a wild ass hunt together, and the lion takes all three shares by rank, partnership and threat.", "predicament": "You went into a venture with a far more powerful partner, and now they are dividing the proceeds entirely their own way.", "moral": "Might makes right."},
{"chunk": 102, "story": 102, "summary": "A lion allies with a dolphin, and when the dolphin cannot come ashore to help him fight, calls him a traitor.", "predicament": "An impressive ally from another field swore friendship with you, and now you need help they have no means to give.", "moral": null},
{"chunk": 103, "story": 103, "summary": "An eagle is mortally shot with an arrow fledged with his own feathers.", "predicament": "You are being brought down by a rival who is using the very tools, ideas or people you supplied them.", "moral": null},
{"chunk": 104, "story": 104, "summary": "A dying kite asks his mother to pray for him, and she asks which god he has not robbed.", "predicament": "In your hour of need you look for someone to speak up for you, and realise you have cheated everyone who could.", "moral": "We must make friends in prosperity if we would have their help in adversity."},
{"chunk": 105, "story": 105, "summary": "A lion and a boar fighting over a well see vultures waiting and make peace.", "predicament": "You and a rival are fighting over something small, and you notice others waiting to pick over whoever loses.", "moral": null},
{"chunk": 106, "story": 106, "summary": "The mice agree to bell the cat, but none will volunteer to do it.", "predicament": "Everyone agrees a clever plan to deal with the danger, and the only question left is who will risk carrying it out.", "moral": null},
{"chunk": 107, "story": 107, "summary": "A one-eyed doe watches the land and is shot from the sea she thought safe.", "predicament": "You have guarded carefully against the threat you expect, and turned your back entirely on the side you assume is safe.", "moral": null},
{"chunk": 108, "story": 108, "summary": "Mice appoint generals who wear conspicuous plumes, and in the rout the generals cannot fit into their holes and are eaten.", "predicament": "You have been promoted and given marks of rank, and when things go wrong those marks may be exactly what trap you.", "moral": "The more honour the more danger."},
{"chunk": 109, "story": 109, "summary": "A shepherd sells his flock to trade dates by sea, loses all in a storm, and later sneers that the calm sea wants more dates.", "predicament": "A calm opening tempts you to sell what you have and stake it all on a trade you do not know.", "moral": null},
{"chunk": 110, "story": 110, "summary": "An ass sees a lion flee at a cock's crow, chases the lion himself, and is torn apart.", "predicament": "You saw someone powerful back down before another person, and are tempted to think they will back down before you too.", "moral": "False confidence often leads into danger."},
{"chunk": 111, "story": 111, "summary": "The rivers complain the sea makes them salty, and the sea tells them to stop flowing into it.", "predicament": "People who keep choosing to join you complain that you change them, and are trying to lay the blame on you.", "moral": null},
{"chunk": 112, "story": 112, "summary": "A boar whets his tusks with no enemy in sight, saying there is no time to sharpen them once attacked.", "predicament": "Things are quiet with no threat in sight, and someone asks why you are spending effort preparing for trouble.", "moral": "To be well prepared for war is the best guarantee of peace."},
{"chunk": 113, "story": 113, "summary": "A milkmaid counts the future profits of her milk, tosses her head at imagined suitors, and spills the pail.", "predicament": "You are carrying your only real asset while already spending, in your head, everything you hope it will turn into.", "moral": null},
{"chunk": 114, "story": 114, "summary": "A bee asks Jupiter for a deadly sting, and gets one that kills her if she uses it.", "predicament": "A powerful patron has promised you any favour, and you are tempted to ask for the power to punish anyone touching what is yours.", "moral": "Evil wishes, like chickens, come home to roost."},
{"chunk": 115, "story": 115, "summary": "A lean wolf admires a well-fed dog until he sees the heavy collar and log, and wants none of it.", "predicament": "A well-fed insider shows you how comfortably their employer keeps them, and you notice the chain they wear for it.", "moral": null},
{"chunk": 116, "story": 116, "summary": "A besieged city asks how to defend itself, and the bricklayer, carpenter and currier each propose their own trade.", "predicament": "In a crisis everyone is asked for the best solution, and each expert recommends exactly what they happen to sell.", "moral": "Every man for himself."},
{"chunk": 117, "story": 117, "summary": "An ass carrying a sacred image thinks the crowd bows to him and balks, until the driver's whip corrects him.", "predicament": "People bow as you pass, and you are tempted to believe it is for you rather than for what you happen to carry.", "moral": "They are not wise who give to themselves the credit due to others."},
{"chunk": 118, "story": 118, "summary": "A storm-bound man kills his sheep, goats and then his oxen, and his dogs flee reasoning they are next.", "predicament": "In hard times your employer has sacrificed even the people who earn for them, and you wonder how long before it is you.", "moral": "He is not to be trusted as a friend who mistreats his own family."},
{"chunk": 119, "story": 119, "summary": "An old hound loses his grip on a boar and, abused by his master, asks to be praised for what he was.", "predicament": "Age has cost you the ability you were known for, and your boss is berating you for failing at it.", "moral": null},
{"chunk": 120, "story": 120, "summary": "A traveller says 'I' found the axe but 'we' are undone when the owner comes, and his companion holds him to 'I'.", "predicament": "A companion kept a find to themselves, and now that it has brought trouble they want you to share the blame.", "moral": "He who shares the danger ought to share the prize."},
{"chunk": 121, "story": 121, "summary": "A dying old lion is struck by the boar and the bull, and then kicked by the ass.", "predicament": "Once powerful, you are now weak, and even the least of those you once ruled are lining up to strike at you.", "moral": null},
{"chunk": 122, "story": 122, "summary": "A wolf sees shepherds eating mutton and remarks on the outcry there would be if he did the same.", "predicament": "The people who condemn you for something are openly doing the very same thing themselves.", "moral": null},
{"chunk": 123, "story": 123, "summary": "Travellers wait for what they think is a ship, and it turns out to be a bundle of sticks.", "predicament": "You have waited a long while for something that looked grand from far away, and it is shrinking as it comes closer.", "moral": "Our mere anticipations of life outrun its realities."},
{"chunk": 124, "story": 124, "summary": "A traveller and the ass's owner fight over who hired the ass's shadow, and the ass runs off.", "predicament": "You and a client are locked in a bitter dispute over a technicality while the thing you both needed slips away.", "moral": "In quarreling about the shadow we often lose the substance."},
{"chunk": 125, "story": 125, "summary": "An overworked ass keeps asking for a new master and each is worse, ending with a tanner.", "predicament": "You are unhappy with your boss and could ask for a transfer, though you have asked before and each move was worse.", "moral": null},
{"chunk": 126, "story": 126, "summary": "Mercury in disguise asks a sculptor the price of his own statue, expecting it to be highest, and is offered it free.", "predicament": "You go incognito to find out how highly people rate you, expecting to hear you are the most valued of all.", "moral": null},
{"chunk": 127, "story": 127, "summary": "A woodcutter hides a fox but points to it while denying it, and the fox leaves without thanks.", "predicament": "Someone claims you owe them for protecting you, when their words protected you and their gestures quietly gave you away.", "moral": null},
{"chunk": 128, "story": 128, "summary": "An uprooted oak asks the reeds how they survive the wind, and they say they bend rather than fight.", "predicament": "A force far stronger than you is sweeping through, and you could stand rigid against it or bend until it passes.", "moral": "Stoop to conquer."},
{"chunk": 129, "story": 129, "summary": "A farmer shuts a lion in his farmyard, loses his flocks, and has to let it go.", "predicament": "You have a chance to trap someone far more dangerous than you, but you would be locked in with them.", "moral": null},
{"chunk": 130, "story": 130, "summary": "A lion takes a stolen lamb from a wolf, who protests the injustice, and the lion jeers at his claim.", "predicament": "Someone stronger has taken from you something you had taken dishonestly yourself, and you want to protest.", "moral": null},
{"chunk": 131, "story": 131, "summary": "A birdcatcher spares his decoy partridge and his cock after each pleads usefulness, but still must feed his guest.", "predicament": "Your boss needs to cut someone and you plead how useful you are, but the need is pressing and must be met somehow.", "moral": "Necessity knows no law."},
{"chunk": 132, "story": 132, "summary": "A dove saves a drowning ant with a leaf, and the ant later stings a birdcatcher to save the dove.", "predicament": "Someone you once helped for nothing sees a threat to you coming that you cannot see, and has a chance to repay you.", "moral": null},
{"chunk": 133, "story": 133, "summary": "Despairing hares rush to drown themselves, see frogs flee from them, and take heart.", "predicament": "You feel so weak and harried that you are ready to give up entirely, and then notice others who fear even you.", "moral": null},
{"chunk": 134, "story": 134, "summary": "A monkey copies the fishermen's netting, gets tangled and drowns.", "predicament": "You have watched experts do a job and are about to try it yourself with their tools, having never learned how.", "moral": null},
{"chunk": 135, "story": 135, "summary": "A swan seized by mistake for the goose sings and is known and spared.", "predicament": "In the dark you have been mistaken for someone marked to be cut, and only showing your one real talent will set it right.", "moral": null},
{"chunk": 136, "story": 136, "summary": "A doe escaping hunters takes refuge in a lion's cave and is killed there.", "predicament": "Fleeing one danger, you are about to take shelter with someone who may be far more dangerous than what chases you.", "moral": "In avoiding one evil, care must be taken not to fall into another."},
{"chunk": 137, "story": 137, "summary": "A fisherman refuses to release a small fish that promises to be worth more when grown.", "predicament": "Someone you have caught asks you to let them go now, promising you a much bigger return later.", "moral": null}
]
@@ -0,0 +1,49 @@
[
{"chunk": 138, "story": 138, "summary": "A timid hunter asks a woodman for the lion's tracks and, offered the lion itself, says he only wanted the tracks.", "predicament": "You have talked boldly about tackling a formidable opponent, and someone offers to take you to face them right now.", "moral": "The hero is brave in deeds as well as words."},
{"chunk": 139, "story": 139, "summary": "A fox gorges on food left in a hollow tree and is too swollen to get out.", "predicament": "You helped yourself so freely to what was lying unguarded that you can no longer get out of the position it put you in.", "moral": null},
{"chunk": 140, "story": 140, "summary": "Two frogs seeking a new home find a deep well, and one asks how they would get out if it dried up.", "predicament": "You and a partner must move, and an attractive opening has appeared that would be very hard to leave if it failed.", "moral": "Do nothing without a regard to the consequences."},
{"chunk": 141, "story": 141, "summary": "A flaring lamp boasts it outshines the sun and is blown out by a gust.", "predicament": "You are shining brightly just now and are tempted to claim you outshine the people who truly lead.", "moral": null},
{"chunk": 142, "story": 142, "summary": "A camel-driver asks his camel whether it would rather go uphill or down, and the camel asks whether the level road is closed.", "predicament": "Your boss offers you a choice between two hard assignments, as if the easy option you both know exists were not there.", "moral": null},
{"chunk": 143, "story": 143, "summary": "A miller and his son change how they travel with their ass at every passer-by's criticism until they lose the ass in the river.", "predicament": "Every passer-by criticises how you are doing the job, and each time you change it to please them, a new critic appears.", "moral": null},
{"chunk": 144, "story": 144, "summary": "A cat pretends to be dead on a peg to lure the mice, and one says it will not come near her even if she turns into a sack.", "predicament": "Someone who has already done you harm now appears harmless, even lifeless, and is plainly waiting for you to come out.", "moral": null},
{"chunk": 145, "story": 145, "summary": "A mouse bites a bull, hides in its hole, and bites him again while he sleeps.", "predicament": "Someone powerful is furious with you but cannot reach you where you are safe, and you could sting them again.", "moral": null},
{"chunk": 146, "story": 146, "summary": "A dog invited by a friend to his master's feast is thrown out by the cook and pretends he was too drunk to remember.", "predicament": "An insider invited you to help yourself at their employer's expense, and you were thrown out and must explain it to your peers.", "moral": null},
{"chunk": 147, "story": 147, "summary": "Thieves steal a cock who pleads he wakes men to work, and they say that is why he must die.", "predicament": "You plead your usefulness to people you have fallen among, not seeing that your usefulness is exactly what they cannot stand.", "moral": "The safeguards of virtue are hateful to those with evil intentions."},
{"chunk": 148, "story": 148, "summary": "Trained monkeys dance like courtiers until someone throws nuts and they brawl for them.", "predicament": "Your team has learned to perform with polish, and a rival is about to toss something in front of them to expose them.", "moral": null},
{"chunk": 149, "story": 149, "summary": "A farmer takes revenge on a fox by setting its tail alight, and it runs through his own wheat.", "predicament": "You finally have the person who has been robbing you at your mercy, and your revenge could easily burn your own harvest.", "moral": null},
{"chunk": 150, "story": 150, "summary": "Fortune wakes a traveller asleep on a well's brink, lest she be blamed for his fall.", "predicament": "You are careless in a dangerous spot, and if you come to grief you will blame bad luck rather than yourself.", "moral": "Everyone is more or less master of his own fate."},
{"chunk": 151, "story": 151, "summary": "A seagull bursts its gullet on too large a fish, and a kite says a bird of the air should not feed from the sea.", "predicament": "You have taken on far more than you can manage in a field that is not yours, and it is tearing you apart.", "moral": "Every man should be content to mind his own business."},
{"chunk": 152, "story": 152, "summary": "A lion and a bear fight over a kid until both are exhausted, and a fox runs off with it.", "predicament": "You and a powerful rival are exhausting each other over one prize, while someone else circles waiting to walk off with it.", "moral": "It sometimes happens that one man has all the toil, and another all the profit."},
{"chunk": 153, "story": 153, "summary": "A philosopher decrying Providence for drowning the innocent with the guilty tramples an anthill for one sting.", "predicament": "You are loudly condemning those above for punishing many for one person's fault, and you have just done the same to those below.", "moral": null},
{"chunk": 154, "story": 154, "summary": "A peasant frees an eagle, which later snatches his bundle to lure him from under a wall that then falls.", "predicament": "Someone you once freed from a trap suddenly snatches something of yours, and you are angry without knowing why they did it.", "moral": null},
{"chunk": 155, "story": 155, "summary": "A leopard and a fox dispute their beauty, and the fox says his is in the mind.", "predicament": "A colleague shows off their visible credentials one by one to prove they are the better, and you have only your wits.", "moral": null},
{"chunk": 156, "story": 156, "summary": "A lion leaves a sleeping hare to chase a hart, loses the hart, and returns to find the hare gone.", "predicament": "You have an easy win within reach, and a bigger, faster prize has just run past that you could chase instead.", "moral": null},
{"chunk": 157, "story": 157, "summary": "A poor carpenter smashes the idol that never helped him, and gold pours out of its head.", "predicament": "You have courted a patron respectfully for years with nothing to show, and anger tempts you to smash the relationship.", "moral": null},
{"chunk": 158, "story": 158, "summary": "A lion eats the ass who divided their spoil equally, and the fox, having learned, leaves himself a morsel.", "predicament": "A powerful partner asks you to divide the spoils, and you have just seen what happened to the last one who divided them fairly.", "moral": "Happy is the man who learns from the misfortunes of others."},
{"chunk": 159, "story": 159, "summary": "A bull hiding from a lion in a cave is butted by a goat, and says he will answer once the lion is gone.", "predicament": "While you are sheltering from a bigger threat, someone lesser takes the chance to attack you, knowing you cannot answer yet.", "moral": "It shows an evil disposition to take advantage of a friend in distress."},
{"chunk": 160, "story": 160, "summary": "A bald knight's wig blows off during a hunt, and he joins the laughter with a joke.", "predicament": "Something you were hiding about yourself has just been exposed in front of your companions, and everyone is laughing.", "moral": null},
{"chunk": 161, "story": 161, "summary": "The oaks complain they are always felled, and Jupiter says it is because they are so useful.", "predicament": "You complain that you are always picked for the hardest jobs, and are told it is because you are so good at them.", "moral": null},
{"chunk": 162, "story": 162, "summary": "A monkey mother smothers her favourite twin with love while the neglected one thrives.", "predicament": "You lavish all your care on one favourite protege and neglect another, and your attention may be what harms the favourite.", "moral": "The best intentions will not always ensure success."},
{"chunk": 163, "story": 163, "summary": "A hound gives up chasing a hare and, mocked, says he ran for a dinner but the hare for his life.", "predicament": "Someone mocks you for losing to a weaker rival, but they were fighting for everything and you only for a little.", "moral": null},
{"chunk": 164, "story": 164, "summary": "A shepherd is about to shut a wolf in the fold, and his dog asks how the sheep can then be safe.", "predicament": "Your boss is about to bring a known predator into the team, and you are the one placed to say so.", "moral": null},
{"chunk": 165, "story": 165, "summary": "An oak split by wedges cut from its own branches grieves more at them than at the axe.", "predicament": "You are being broken apart by people who came from your own side, and it hurts more than any outside attack.", "moral": "Misfortunes springing from ourselves are the hardest to bear."},
{"chunk": 166, "story": 166, "summary": "A snake tormented by a wasp puts its head under a wagon wheel so they die together.", "predicament": "Someone is tormenting you without pause and you cannot shake them, and you could take them down with you.", "moral": null},
{"chunk": 167, "story": 167, "summary": "A peacock mocks a plain crane's plumage, and the crane says he flies to the heavens while the peacock walks the dunghill.", "predicament": "A showy colleague mocks your plain appearance in front of others, and you could answer with what you can actually do.", "moral": "Fine feathers don’t make fine birds."},
{"chunk": 168, "story": 168, "summary": "A couple kill their hen that lays golden eggs to get the gold inside and find nothing.", "predicament": "Something steadily brings you a small, sure return, and you are tempted to break it open to get everything at once.", "moral": null},
{"chunk": 169, "story": 169, "summary": "An ass who falls in a pond groans, and the frogs ask what he would do if he lived there always.", "predicament": "You are making a great fuss about a small setback in front of people who live with far worse every day.", "moral": "Men often bear little grievances with less courage than they do large misfortunes."},
{"chunk": 170, "story": 170, "summary": "A crow jealous of the raven's standing as an omen caws at travellers, who dismiss her cry.", "predicament": "Envying a colleague whose word carries weight, you are tempted to speak up as if yours carried the same.", "moral": "Those who assume a character which does not belong to them, only make themselves ridiculous."},
{"chunk": 171, "story": 171, "summary": "The trees give a man a young ash for his axe handle, and he fells the great trees with it.", "predicament": "Someone asks your group to give up just one small, junior member, and that concession is what they need to cut down the rest.", "moral": null},
{"chunk": 172, "story": 172, "summary": "Wolves urge the sheepdogs to betray the flock and live as brothers, and then tear the dogs apart.", "predicament": "Rivals flatter you as one of their own and urge you to hand over what you guard and share the spoils with them.", "moral": null},
{"chunk": 173, "story": 173, "summary": "A lioness mourns her cub killed by a bull, and a hunter reminds her how many she has made mourn.", "predicament": "You are loudly grieving a loss, and someone reminds you how many people have suffered the same at your hands.", "moral": null},
{"chunk": 174, "story": 174, "summary": "A lion wounded by a bowman's arrow flees, saying he dare not face the man if that is his messenger.", "predicament": "An opponent's first warning shot has already hurt you badly, and an adviser urges you not to give up at the first blow.", "moral": "Be on guard against men who can strike from a distance."},
{"chunk": 175, "story": 175, "summary": "Men first flee the huge camel, then, finding it meek, bridle it and let a child drive it.", "predicament": "People who once feared you have noticed you never push back, and are growing bolder by the day.", "moral": "Use serves to overcome dread."},
{"chunk": 176, "story": 176, "summary": "A crab that moved inland to feed is eaten by a fox and blames himself for leaving the sea.", "predicament": "You left the field you are made for to chase better prospects elsewhere, and now you are exposed and out of your depth.", "moral": "Contentment with our lot is an element of happiness."},
{"chunk": 177, "story": 177, "summary": "An ass refuses to flee the enemy with his master, since any master will load him the same.", "predicament": "Your boss urges you to fight off a takeover, and you suspect any new owner would work you exactly the same.", "moral": "In a change of government the poor change nothing beyond the name of their master."},
{"chunk": 178, "story": 178, "summary": "A wounded fox refuses to have the sated flies driven off, lest hungrier ones replace them.", "predicament": "Someone offers to rid you of the people draining you, but those ones are already well fed and hungrier ones would follow.", "moral": null},
{"chunk": 179, "story": 179, "summary": "A woman doubles her hen's feed to get two eggs a day, and it grows fat and stops laying.", "predicament": "A reliable source gives you a steady return, and you are thinking of pushing extra into it to double what it produces.", "moral": null},
{"chunk": 180, "story": 180, "summary": "Kites that could once sing try to neigh like horses and lose their song.", "predicament": "You are so taken with a colleague's style that you are trying to copy it, and your own gift is starting to fade.", "moral": "The desire for imaginary benefits often involves the loss of present blessings."},
{"chunk": 181, "story": 181, "summary": "A hound alternately bites and fawns on a hare, who asks him to show his true colours.", "predicament": "Someone alternately attacks you and flatters you, and you cannot tell whether to treat them as friend or enemy.", "moral": "No one can be a friend if you know not whether to trust or distrust him."},
{"chunk": 182, "story": 182, "summary": "Hares at war with eagles ask the foxes for help, who decline knowing both sides.", "predicament": "Someone weak who has picked a fight with the powerful asks you to join their side.", "moral": "Count the cost before you commit yourselves."},
{"chunk": 183, "story": 183, "summary": "A calf offers to show a bull the way through a narrow passage, and the bull says he knew it before the calf was born.", "predicament": "A newcomer offers to show you how to do something you have been doing since before they arrived.", "moral": null},
{"chunk": 184, "story": 184, "summary": "A stag asks a sheep for a loan with the wolf as surety, and the sheep declines, doubting she could ever collect.", "predicament": "Someone asks you for a loan and offers as guarantor another person well known for taking what they want and vanishing.", "moral": "Two blacks do not make one white."}
]
@@ -0,0 +1,45 @@
[
{"chunk": 185, "story": 185, "summary": "A cat tells an eagle and a sow each that the other plans to kill her young, and both starve in fear while the cat feeds.", "predicament": "A colleague privately warns you that your neighbour is plotting against you, and you suspect they said the same to your neighbour.", "moral": null},
{"chunk": 186, "story": 186, "summary": "A huge wolf nicknamed 'Lion' leaves the wolves to live among lions, and a fox mocks his conceit.", "predicament": "Your peers jokingly call you the equal of those at the top, and you are tempted to leave them and move among the top.", "moral": null},
{"chunk": 187, "story": 187, "summary": "An idle, overfed mule boasts he is a racer's son, and after a long hard journey decides his father was an ass.", "predicament": "After an easy spell you have been boasting of your pedigree and speed, and tomorrow brings a long, hard assignment.", "moral": null},
{"chunk": 188, "story": 188, "summary": "A fortune-teller runs home on hearing his house is being robbed, and a neighbour asks why he did not foresee it.", "predicament": "You make your living advising others on what is coming, and a disaster has just struck you that you failed to foresee.", "moral": null},
{"chunk": 189, "story": 189, "summary": "A frog refuses a friend's invitation to leave his exposed gully for a safe pond and is crushed by a wagon.", "predicament": "A friend urges you to leave your familiar but exposed position for a safer one, and you cannot bring yourself to move.", "moral": "A willful man will have his way to his own hurt."},
{"chunk": 190, "story": 190, "summary": "A countryman frees an eagle from a serpent, and the eagle later knocks away the drink the serpent poisoned.", "predicament": "You stepped in to rescue someone from a dangerous enemy, and that enemy has quietly set a trap for you in return.", "moral": null},
{"chunk": 191, "story": 191, "summary": "A thirsty crow raises the water in a pitcher by dropping in stones one by one.", "predicament": "What you urgently need is just out of reach, and none of the obvious approaches has worked.", "moral": "Necessity is the mother of invention."},
{"chunk": 192, "story": 192, "summary": "A thief frightens an innkeeper with a tale that he turns into a wolf, and makes off with his coat.", "predicament": "A stranger tells you an alarming story and urges you to act at once, and the action would leave your goods in their hands.", "moral": "Every tale is not to be believed."},
{"chunk": 193, "story": 193, "summary": "A hart hidden under a vine eats its leaves once the hunters pass, and is spotted and shot.", "predicament": "Someone sheltered you when you were in danger, and now that the danger seems past you are tempted to help yourself to their things.", "moral": null},
{"chunk": 194, "story": 194, "summary": "A gnat stings a lion into tearing his own face, then is caught and eaten by a spider.", "predicament": "You have just beaten a powerful opponent and are celebrating loudly, not watching for small, patient dangers.", "moral": null},
{"chunk": 195, "story": 195, "summary": "A fox that cannot reach the grapes walks away saying they are sour.", "predicament": "You tried everything to get something you wanted and failed, and people are watching to see how you take it.", "moral": null},
{"chunk": 196, "story": 196, "summary": "A roadside walnut tree is beaten with sticks by passers-by who want its nuts.", "predicament": "The more you give, the more roughly people treat you to get more, and those you feed are the ones doing the damage.", "moral": null},
{"chunk": 197, "story": 197, "summary": "A kid caught by a wolf asks him to pipe a last dance, and the music brings hounds that chase off the wolf.", "predicament": "Someone who has you cornered is enjoying their power, and you could ask for one small favour that might draw help.", "moral": null},
{"chunk": 198, "story": 198, "summary": "A dolphin carrying a monkey from a wreck drowns him when he lies about his noble birth and 'knowing' the Piraeus.", "predicament": "Someone is helping you because they think you are one of their own, and asks questions you can only answer by lying.", "moral": null},
{"chunk": 199, "story": 199, "summary": "A horse accepts a man's bit and saddle to get revenge on a stag, and is enslaved for good.", "predicament": "To get back at a newcomer who shares your territory, someone powerful offers help if you accept their harness.", "moral": null},
{"chunk": 200, "story": 200, "summary": "A jackdaw painted white lives among doves until he chatters, and is then rejected by both doves and jackdaws.", "predicament": "You have disguised yourself as one of a better-fed group to share their benefits, and must keep silent to pass.", "moral": null},
{"chunk": 201, "story": 201, "summary": "A monkey claims a cemetery's monuments honour his ancestors, and a fox notes no ancestor can contradict him.", "predicament": "You are inflating your background to a companion, and they have noticed that nobody who could contradict you is around.", "moral": "A false tale often betrays itself."},
{"chunk": 202, "story": 202, "summary": "A man sends his disliked wife to her father's house, and her report of the herdsmen's looks confirms her effect on everyone.", "predicament": "Someone you are responsible for is disliked by everyone around them, and you want to learn whether it is them or the place.", "moral": "Straws show how the wind blows."},
{"chunk": 203, "story": 203, "summary": "A horse, ox and dog repay a man's shelter by giving the stages of his life their own tempers.", "predicament": "People you took in during hard times want to repay you by shaping you in their own likeness.", "moral": null},
{"chunk": 204, "story": 204, "summary": "A thief throws meat to a housedog, who says the sudden kindness only makes him more watchful.", "predicament": "A stranger showers you with sudden favours just as you are guarding something for your employer.", "moral": null},
{"chunk": 205, "story": 205, "summary": "An ape king rewards a liar's flattery and has his court maul a truthful traveller.", "predicament": "A vain leader asks your honest opinion of them and their court, and has just richly rewarded someone who flattered them.", "moral": null},
{"chunk": 206, "story": 206, "summary": "A fox terrified at first sight of a lion grows bolder each meeting until he chats with him.", "predicament": "You were terrified the first time you met someone very powerful, and each meeting since, you have been growing bolder.", "moral": "Acquaintance softens prejudices."},
{"chunk": 207, "story": 207, "summary": "An old weasel rolls in flour to look like food, and an old mouse sees through it.", "predicament": "Something tempting lies in a dark corner, and you notice that everyone who went for it has vanished.", "moral": null},
{"chunk": 208, "story": 208, "summary": "A drowning boy asks a traveller who scolds him to help first and scold afterwards.", "predicament": "You are in serious trouble and the person who could help is busy lecturing you on how you got there.", "moral": "Counsel without help is useless."},
{"chunk": 209, "story": 209, "summary": "A peacock complains to Juno that he cannot sing, and she tells him each has his own gift.", "predicament": "You are admired for one gift but resent being laughed at for lacking another that a colleague has.", "moral": null},
{"chunk": 210, "story": 210, "summary": "A wolf follows a flock harmlessly for so long that the shepherd leaves him in charge, and he kills the flock.", "predicament": "Someone you once guarded against has behaved so well for so long that you are about to leave them in charge.", "moral": null},
{"chunk": 211, "story": 211, "summary": "Hares argue that all beasts should be equal, and lions say the words lack claws and teeth.", "predicament": "You are arguing before the powerful that everyone should be treated as equals, and you have nothing to back it with.", "moral": null},
{"chunk": 212, "story": 212, "summary": "A man selling an image of Mercury claims it brings wealth, and says he sells because its gifts come too slowly.", "predicament": "You are selling something by promising it makes its owner rich, and a customer asks why you would part with it.", "moral": null},
{"chunk": 213, "story": 213, "summary": "A hawk refuses a nightingale's plea to let him go for larger birds not yet in sight.", "predicament": "You have someone in your power who argues you should let them go and chase a bigger target instead.", "moral": null},
{"chunk": 214, "story": 214, "summary": "A lark stays put while the farmer relies on friends to reap, and moves when he says he will reap himself.", "predicament": "Your boss keeps announcing a shake-up, and you must judge whether they mean it this time or will leave it to others again.", "moral": "Self-help is the best help."},
{"chunk": 215, "story": 215, "summary": "A fox flatters a cock in a tree, who invites him to wake the 'porter' below, and the dog kills the fox.", "predicament": "A stranger known to prey on people like you is praising you warmly and asking to be let in to meet you properly.", "moral": null},
{"chunk": 216, "story": 216, "summary": "Cranes fly off at a birdcatcher's approach, while the heavier geese are caught.", "predicament": "Danger is coming for everyone in your position, and the nimble are already slipping away while you are weighed down.", "moral": null},
{"chunk": 217, "story": 217, "summary": "An ass feigns a thorn in his hoof, and when the wolf goes to pull it he kicks his teeth in.", "predicament": "Someone stronger has you cornered, and you could ask them for a small service that would put them off their guard.", "moral": null},
{"chunk": 218, "story": 218, "summary": "A goat envious of the ass advises him to feign illness, and the goat's lungs are then prescribed to heal him.", "predicament": "Out of envy you are urging a colleague to fake an illness to escape work, and the fallout could land on you.", "moral": null},
{"chunk": 219, "story": 219, "summary": "A lion invites a bull to dine on a sheep, and the bull sees only spits and cauldrons big enough for a bull.", "predicament": "Someone who would gain by your downfall invites you to share a feast, and the preparations look suspiciously large.", "moral": null},
{"chunk": 220, "story": 220, "summary": "A fox finds a lifelike mask in an actor's house and says it is beautiful but has no brains.", "predicament": "An impressive-looking candidate is presented to you, and you notice there seems to be nothing behind the face.", "moral": null},
{"chunk": 221, "story": 221, "summary": "An owl kept awake by a grasshopper flatters her voice and invites her to drink, then kills her.", "predicament": "Someone you have been annoying suddenly praises you and invites you in for a treat, after asking you nicely to stop.", "moral": null},
{"chunk": 222, "story": 222, "summary": "A fowler staring up at a thrush treads on a viper and is fatally stung.", "predicament": "You are so intent on a target you are stalking that you have stopped watching where you are putting your own feet.", "moral": null},
{"chunk": 223, "story": 223, "summary": "A proud horse threatens a laden ass, then broken-winded is put to a dungcart and mocked by the ass.", "predicament": "Someone high in favour sneers at you and your heavy load, and you could answer back or hold your peace.", "moral": null},
{"chunk": 224, "story": 224, "summary": "A lion afraid of three bulls together separates them with sly talk and eats them one by one.", "predicament": "Someone who fears you and your colleagues together is quietly talking each of you into going your own way.", "moral": "Union is strength."},
{"chunk": 225, "story": 225, "summary": "A wolf urges a goat on a cliff to come down to the tender grass, and she knows he wants to eat her.", "predicament": "You hold a secure position, and someone who would profit from your fall urges you down to where the rewards are richer.", "moral": null},
{"chunk": 226, "story": 226, "summary": "A fly on the axle threatens to sting the mule, who says he heeds only the driver.", "predicament": "Someone with no authority over you is loudly telling you to hurry up and threatening you if you do not.", "moral": null},
{"chunk": 227, "story": 227, "summary": "Fishermen rejoice at heavy nets that turn out full of stones, and an old man says sorrow is twin to joy.", "predicament": "You celebrated early over what looked like a huge haul, and it has turned out to be mostly worthless.", "moral": null}
]
@@ -0,0 +1,47 @@
[
{"chunk": 228, "story": 228, "summary": "A country mouse visits his friend's rich town house, is chased from every meal, and goes back to his safe plain fare.", "predicament": "A well-off friend urges you to leave your plain, safe post for their lavish one, where every meal comes with a fright.", "moral": null},
{"chunk": 229, "story": 229, "summary": "An ape judging a wolf's theft charge against a fox rules that the wolf lost nothing and the fox stole anyway.", "predicament": "You must settle a dispute between two people with bad reputations, and you believe neither of their accounts.", "moral": "The dishonest, if they act honestly, get no credit."},
{"chunk": 230, "story": 230, "summary": "Wasps and partridges promise a farmer service for water, and he says his oxen already do it without promises.", "predicament": "Newcomers promise you great service in return for a favour now, while your steady workers already do it without promising.", "moral": null},
{"chunk": 231, "story": 231, "summary": "A father tells his vain son and plain, spiteful daughter both to look in the mirror daily, to mind their conduct.", "predicament": "Two people in your care are at odds, one vain of their gifts and the other spiteful for lacking them, and both appeal.", "moral": null},
{"chunk": 232, "story": 232, "summary": "Dogs tear at a lion's skin, and a fox says they would learn a lion's claws if it were alive.", "predicament": "Someone who once ruled over you is gone for good, and everyone is now bravely tearing into their reputation.", "moral": "It is easy to kick a man that is down."},
{"chunk": 233, "story": 233, "summary": "A blind man feeling a wolf cub cannot say if it is fox or wolf, but knows it must not go in the sheepfold.", "predicament": "You are asked to vouch for a young newcomer whose record is unclear, but whose instincts are plainly those of a predator.", "moral": "Evil tendencies are shown in early life."},
{"chunk": 234, "story": 234, "summary": "A failed cobbler poses as a doctor with a cure-all, and confesses when the governor tests him with 'poison'.", "predicament": "Your reputation as an expert was built on noise rather than skill, and someone powerful is about to test it on you.", "moral": null},
{"chunk": 235, "story": 235, "summary": "A wolf says he left a field of oats untouched for the horse, who says he would not if wolves ate oats.", "predicament": "Someone with a bad reputation makes a show of having saved something for you that they could never have used themselves.", "moral": "Men of evil reputation, when they perform a good deed, fail to get credit for it."},
{"chunk": 236, "story": 236, "summary": "Two enemies on a sinking ship sit at opposite ends, and one is content if his enemy's end sinks first.", "predicament": "You and a bitter enemy are going down together, and you care more about seeing them fall first than about saving yourself.", "moral": null},
{"chunk": 237, "story": 237, "summary": "A partridge pecked by gamecocks thinks it is because he is a stranger, until he sees them fight each other.", "predicament": "You are new and the old hands keep attacking you, and you assume it is because you are an outsider.", "moral": null},
{"chunk": 238, "story": 238, "summary": "A fox reviles a caged lion, who says it is his mischance, not the fox, that insults him.", "predicament": "Someone powerful is trapped by circumstances for now, and it is tempting to insult them while they cannot answer.", "moral": null},
{"chunk": 239, "story": 239, "summary": "A frog proclaims himself a physician, and a fox asks why he cannot cure his own gait and skin.", "predicament": "Someone offers to fix everyone else's problems, while their own obvious failings are there for all to see.", "moral": null},
{"chunk": 240, "story": 240, "summary": "A wolf accuses the absent fox to the sick lion, and the fox prescribes the wolf's warm skin as the cure.", "predicament": "A rival is denouncing you to the boss just as you walk in, and you could turn the boss's anger back on them.", "moral": null},
{"chunk": 241, "story": 241, "summary": "A dog curled against winter cold plans a house, but stretched out in summer thinks one too big a job.", "predicament": "In hard times you resolved to build yourself some security, and now that times are easy it seems too big a job.", "moral": null},
{"chunk": 242, "story": 242, "summary": "The north wind blows in vain to strip a traveller's cloak, and the sun's warmth makes him take it off.", "predicament": "You need someone to give something up, and the harder you push the tighter they cling to it.", "moral": "Persuasion is better than Force."},
{"chunk": 243, "story": 243, "summary": "A crow breaks his vow to Apollo after being freed, and when caught again Mercury will not believe his new vow.", "predicament": "You broke your promise to the last person who got you out of trouble, and now you need a new patron to trust you.", "moral": null},
{"chunk": 244, "story": 244, "summary": "A fox serves a crane soup in a flat dish, and the crane repays him with a narrow-necked jar.", "predicament": "Someone humiliated you with a show of hospitality you could not use, and now it is your turn to host them.", "moral": null},
{"chunk": 245, "story": 245, "summary": "A wolf seeing his long evening shadow thinks he should be king, and a lion kills him.", "predicament": "Things are going so well that you feel bigger than the people above you and start to think you deserve their place.", "moral": null},
{"chunk": 246, "story": 246, "summary": "A bat fights for whichever side, birds or beasts, is winning, and after peace both condemn him.", "predicament": "Two factions are at war, and you could keep switching to whichever side is winning at the moment.", "moral": null},
{"chunk": 247, "story": 247, "summary": "A spendthrift sells his last cloak on seeing an early swallow, and the frost returns.", "predicament": "Having run through almost everything, you see an early sign of good times and are tempted to sell your last safeguard.", "moral": null},
{"chunk": 248, "story": 248, "summary": "A captured trumpeter pleads he carried no weapon, and his captors say his trumpet roused all the others.", "predicament": "You are caught on the losing side and plead that you never did harm yourself, only urged others on.", "moral": null},
{"chunk": 249, "story": 249, "summary": "The birds scorn the owl's warnings about acorns, flax and archers until they come true.", "predicament": "You warned your group early about dangers others could not yet see, and they called you mad.", "moral": null},
{"chunk": 250, "story": 250, "summary": "Driven out by the ills, the goods ask Jupiter for protection, and he sends them to men one at a time.", "predicament": "Troubles keep arriving together while good things come one at a time, and you must learn to spot the good when it comes.", "moral": null},
{"chunk": 251, "story": 251, "summary": "An ass in a lion's skin frightens the foolish beasts until a fox hears him bray.", "predicament": "You have borrowed the trappings of real authority and are enjoying frightening people, and now you meet someone who listens.", "moral": null},
{"chunk": 252, "story": 252, "summary": "A sparrow mocks a hare seized by an eagle and is himself taken by a hawk.", "predicament": "A colleague has just been brought down, and it is tempting to mock them for failing, as if it could not happen to you.", "moral": null},
{"chunk": 253, "story": 253, "summary": "A flea asks why the ox slaves for men, and the ox says he is loved and patted, while a pat means the flea's death.", "predicament": "A freeloader asks why you work so hard for your employer when they themselves take freely and give nothing.", "moral": null},
{"chunk": 254, "story": 254, "summary": "A buyer returns an ass on trial because it at once joined the laziest ass in the yard.", "predicament": "You are new and on trial, and the first thing you do is choose whom to fall in with.", "moral": "A man is known by the company he keeps."},
{"chunk": 255, "story": 255, "summary": "A caged dove boasts of her many young, and a crow says that only means more of them in prison.", "predicament": "You boast of how many people you have brought into the organisation, when all of them are stuck there with you.", "moral": null},
{"chunk": 256, "story": 256, "summary": "A satyr breaks his friendship with a man who blows on his hands to warm them and on his food to cool it.", "predicament": "Your new ally has watched you argue both sides of the same question within a day, and is doubting you.", "moral": null},
{"chunk": 257, "story": 257, "summary": "Momus, made judge of the gods' works, envies and faults all three, and is expelled.", "predicament": "You have been asked to judge between colleagues' work, and your envy is tempting you to find fault with all of it.", "moral": null},
{"chunk": 258, "story": 258, "summary": "A jackdaw envious of an eagle tries to carry off a ram, gets caught in the fleece, and is clipped and given to children.", "predicament": "You watched a star performer pull off a feat and are about to try an even bigger one yourself, without their strength.", "moral": null},
{"chunk": 259, "story": 259, "summary": "An eagle steals her neighbour fox's cub, and later her own young burn in a nest fired by a stolen cinder.", "predicament": "A powerful neighbour who swore friendship has taken something precious from you, and you cannot reach them to answer it.", "moral": null},
{"chunk": 260, "story": 260, "summary": "Every man carries a bag of his neighbours' faults in front and a bigger bag of his own behind.", "predicament": "You see a colleague's faults very clearly and are ready to point them out, without looking at your own.", "moral": "Hence it is that men are quick to see the faults of others, and yet are often blind to their own failings."},
{"chunk": 261, "story": 261, "summary": "A bitch granted a place to whelp later uses her grown pups to bar the shepherd from it.", "predicament": "Someone you let use your space for a while has built up a following and now claims it as their own.", "moral": null},
{"chunk": 262, "story": 262, "summary": "A stag admires his antlers and despises his thin legs, which save him until the antlers catch in a thicket.", "predicament": "You prize your showy strengths and are ashamed of your plain ones, and trouble is coming that will test which matters.", "moral": "What is most truly valuable is often underrated."},
{"chunk": 263, "story": 263, "summary": "A lark born before the earth, with nowhere to bury her father, buries him in her own head.", "predicament": "You owe a duty of respect to someone who has gone, and there is no proper place to pay it but within yourself.", "moral": "Youth’s first duty is reverence to parents."},
{"chunk": 264, "story": 264, "summary": "A gnat asks a bull if he should leave, and the bull says he never noticed him come.", "predicament": "You are making a show of whether to leave, sure your departure will be felt, and you may not have been noticed at all.", "moral": "Some men are of more consequence in their own eyes than in the eyes of their neighbors."},
{"chunk": 265, "story": 265, "summary": "A camel envious of the monkey's applauded dance tries to dance and is driven out.", "predicament": "A colleague's performance has just been loudly applauded, and you are tempted to get up and try to outdo them.", "moral": "It is absurd to ape our betters."},
{"chunk": 266, "story": 266, "summary": "Hungry dogs try to drink a river dry to reach hides in it and burst.", "predicament": "Your group has agreed on a plan to reach something it wants that would require doing the impossible.", "moral": "Attempt not impossibilities."},
{"chunk": 267, "story": 267, "summary": "A hungry jackdaw waits on a fig tree for out-of-season figs to ripen.", "predicament": "You are waiting, hungry, for a promise that is out of season to come good, and someone tells you it never will.", "moral": null},
{"chunk": 268, "story": 268, "summary": "An honest woodman is given gold and silver axes, and a neighbour who lies to get the same loses his own axe.", "predicament": "A colleague was rewarded for honesty in a lucky accident, and you could stage the same accident and claim the reward.", "moral": null},
{"chunk": 269, "story": 269, "summary": "A peasant ignores the pleas of birds to spare his barren tree, but spares it on finding honey inside.", "predicament": "You are about to cut something that has not paid its way, and those who depend on it beg you to spare it.", "moral": "Self-interest alone moves some men."},
{"chunk": 270, "story": 270, "summary": "A soldier who fled a robber returns boasting after his comrade has killed him, and is rebuked.", "predicament": "You ran when your partner was in danger, and now that it is over you are tempted to boast of your courage.", "moral": null},
{"chunk": 271, "story": 271, "summary": "A shepherd shakes down acorns onto his cloak, and the sheep eating them tear it.", "predicament": "The people you work to provide for have carelessly damaged something of yours while taking what you gave them.", "moral": null},
{"chunk": 272, "story": 272, "summary": "The gods choose fruitless trees so as not to seem covetous, and Minerva prefers the olive for its fruit.", "predicament": "Your peers choose honours that look disinterested, and you would rather be known for work that is plainly useful.", "moral": null}
]
@@ -0,0 +1,42 @@
[
{"chunk": 273, "story": 273, "summary": "A wrestler bitten by a flea calls on Hercules, and wonders what help to expect against greater foes if none comes for a flea.", "predicament": "You keep calling on your powerful backer over every small irritation, and wonder whether they will come when it truly matters.", "moral": null},
{"chunk": 274, "story": 274, "summary": "A fox who finds prey for a lion grows jealous of the lion's share, hunts alone, and is caught.", "predicament": "You do the scouting while a stronger partner takes the biggest share, and you are tempted to go it alone.", "moral": null},
{"chunk": 275, "story": 275, "summary": "A traveller meets Truth alone in the desert, who says she left the city because falsehood is now with everyone.", "predicament": "Everyone around you now lies as a matter of course, and speaking plainly has left you entirely on your own.", "moral": null},
{"chunk": 276, "story": 276, "summary": "A murderer fleeing his victim's kin climbs from a lion into a serpent's tree and jumps into a crocodile's river.", "predicament": "Pursued for a real wrong you did, you find every place you run to is more dangerous than the last.", "moral": null},
{"chunk": 277, "story": 277, "summary": "An eagle proposes an alliance, and the lion asks for a surety since the eagle could fly off whenever he likes.", "predicament": "Someone proposes a partnership who could walk away from it at any moment, while you could not.", "moral": "Try before you trust."},
{"chunk": 278, "story": 278, "summary": "An ass bolts for a precipice, and his owner, unable to hold him by the tail, lets go.", "predicament": "Someone you are responsible for is determined to throw themselves at a ruinous course, and holding them back is hurting you both.", "moral": null},
{"chunk": 279, "story": 279, "summary": "A thrush lingers on a myrtle for its berries and is caught by a fowler.", "predicament": "You have been lingering in one pleasant spot because it is so rewarding, and someone has noticed exactly where to find you.", "moral": null},
{"chunk": 280, "story": 280, "summary": "A wolf waits all day at a cottage door after hearing a mother threaten to throw her child out to him.", "predicament": "Someone made an angry threat in your hearing that seemed to promise you a windfall, and you are waiting for them to follow through.", "moral": null},
{"chunk": 281, "story": 281, "summary": "A hen hatches viper eggs, and a swallow warns they will harm everyone, starting with her.", "predicament": "You are carefully nurturing a group of newcomers who, once grown, are likely to turn on everyone, starting with you.", "moral": null},
{"chunk": 282, "story": 282, "summary": "An amaranth envies the rose, who replies that her beauty is brief while the amaranth never fades.", "predicament": "You envy a colleague's dazzling reputation, and they tell you it will not last while your quieter one will.", "moral": null},
{"chunk": 283, "story": 283, "summary": "Travellers resting in a plane tree's shade call it useless, and the tree rebukes them.", "predicament": "People who depend on you every day are, in your hearing, calling you useless because you do not produce anything showy.", "moral": "Some men underrate their best blessings."},
{"chunk": 284, "story": 284, "summary": "A horse refuses the ass a little feed now but promises a sackful later, and the ass does not believe it.", "predicament": "Someone refuses you a small favour now but promises a much larger one later, and you are asked to be grateful.", "moral": null},
{"chunk": 285, "story": 285, "summary": "A crow rides a sheep's back and says she bullies the weak and flatters the strong.", "predicament": "A colleague bullies you freely, and openly admits they would never try it with someone who could bite back.", "moral": null},
{"chunk": 286, "story": 286, "summary": "A slipping fox grabs a bramble for help, is torn by it, and is told he should have known its nature.", "predicament": "In a crisis you grabbed for help from someone known to take advantage of everyone, and they have hurt you.", "moral": null},
{"chunk": 287, "story": 287, "summary": "An ass envies the pampered warhorse until the horse is killed in battle.", "predicament": "You envy how well a favoured colleague is kept, not asking what they are being kept for.", "moral": null},
{"chunk": 288, "story": 288, "summary": "A lion ashamed of fearing a cock finds an elephant terrified of a gnat, and is consoled.", "predicament": "You are strong in every way but one small fear, and it shames you so much that you are ready to give up.", "moral": null},
{"chunk": 289, "story": 289, "summary": "A dog used to eggs swallows an oyster whole and suffers for thinking everything round is an egg.", "predicament": "Something new looks just like what you are used to handling, and you are about to take it on without a second look.", "moral": "They who act without sufficient thought, will often fall into unsuspected danger."},
{"chunk": 290, "story": 290, "summary": "Robbers wound the proud mule carrying treasure and ignore the one carrying grain.", "predicament": "You are proud to carry the most valuable load and make sure everyone hears it, while a colleague carries theirs quietly.", "moral": null},
{"chunk": 291, "story": 291, "summary": "A lamb fleeing a wolf into a temple says being sacrificed there is better than being eaten by him.", "predicament": "Someone chasing you warns that the refuge you are running to will be no kinder to you than they would be.", "moral": null},
{"chunk": 292, "story": 292, "summary": "A partridge offers to lure other partridges if spared, and the fowler kills him for it.", "predicament": "You have been caught, and you could save yourself by offering to deliver your colleagues to the one who caught you.", "moral": null},
{"chunk": 293, "story": 293, "summary": "A man catches a flea who pleads it does little harm, and kills it because no evil should be tolerated.", "predicament": "You have caught someone in a petty offence, and they plead that what they did was too small to matter.", "moral": null},
{"chunk": 294, "story": 294, "summary": "A rich man presses his tanner neighbour to move, the tanner stalls, and the rich man grows used to the smell.", "predicament": "A powerful neighbour wants you gone and keeps pressing, and you could keep promising to leave soon without actually going.", "moral": null},
{"chunk": 295, "story": 295, "summary": "A viper asks a file for a meal, and the file says he takes from everyone and gives nothing.", "predicament": "You have gone to ask for help from someone well known for taking from everyone and never giving.", "moral": "The covetous are poor givers."},
{"chunk": 296, "story": 296, "summary": "A shepherd pulls a thorn from a lion's paw, and the lion later spares him in the arena.", "predicament": "Someone dangerous and in pain comes to you for help, and helping them means getting close to their claws.", "moral": null},
{"chunk": 297, "story": 297, "summary": "A camel envying the bull's horns asks Jupiter for some and loses part of his ears.", "predicament": "Not content with your strengths, you are asking the boss for a colleague's distinctions too.", "moral": null},
{"chunk": 298, "story": 298, "summary": "A panther in a pit, stoned by some shepherds and fed by others, escapes and takes revenge only on his attackers.", "predicament": "Someone dangerous has fallen, and others are pelting them while a few offer help, and you must choose which you will be.", "moral": null},
{"chunk": 299, "story": 299, "summary": "A kite boasts he can catch ostriches to win an eagle's hand, and brings her a stale mouse.", "predicament": "Someone courting you for a partnership boasts of what they can deliver, and you have no way yet to check.", "moral": null},
{"chunk": 300, "story": 300, "summary": "An eagle freed by a new owner brings him a hare, and a fox advises courting the old owner who clipped him.", "predicament": "A new employer has restored what your old one took from you, and you must decide whom to cultivate.", "moral": null},
{"chunk": 301, "story": 301, "summary": "A prince confined because of his father's dream of a lion strikes at a painted lion, is pricked by a thorn and dies.", "predicament": "You have been confined to protect you from a danger someone only dreamed of, and you are raging against it.", "moral": "We had better bear our troubles bravely than try to escape them."},
{"chunk": 302, "story": 302, "summary": "A cat turned into a woman for love leaps from the bridal couch after a mouse.", "predicament": "You have been given a new, polished role, and an old temptation is about to be dropped in front of you.", "moral": "Nature exceeds nurture."},
{"chunk": 303, "story": 303, "summary": "A beetle whose young an eagle ate pursues her eggs even into Jupiter's lap until they are broken.", "predicament": "Someone far more powerful destroyed what you cared about, and you could pursue them relentlessly even into their protector's presence.", "moral": "The weak often revenge themselves on those who use them ill, even though they be more powerful."},
{"chunk": 304, "story": 304, "summary": "The he-goats complain the she-goats got beards, and Jupiter says an empty badge makes them no equals.", "predicament": "Juniors have been given the same badge of rank as you, and you are tempted to complain to those above.", "moral": "It matters little if those who are inferior to us in merit should be like us in outside appearances."},
{"chunk": 305, "story": 305, "summary": "A bald man slaps his own head at a fly, and says he forgives himself but would kill the fly at any cost.", "predicament": "Someone small keeps needling you, and your efforts to hit back are hurting you more than them.", "moral": null},
{"chunk": 306, "story": 306, "summary": "A shipwrecked man blames the sea, who says the winds are to blame for her fury.", "predicament": "You were ruined in a venture and blame the institution, which says it was only stirred up by others.", "moral": null},
{"chunk": 307, "story": 307, "summary": "A crowd prefers a buffoon's fake pig squeal to a countryman's real pig, until he shows it.", "predicament": "The crowd has already decided their favourite is better than you, however genuine your work, and you must show them.", "moral": null},
{"chunk": 308, "story": 308, "summary": "A hungry crow seizes a sleeping serpent as a windfall and is bitten to death.", "predicament": "You are desperate, and an easy prize seems to have been left lying unguarded right in front of you.", "moral": null},
{"chunk": 309, "story": 309, "summary": "A horseman takes a hunter's hare under pretence of buying it and gallops off, and the hunter calls it a gift.", "predicament": "Someone has made off with what you earned, pretending to buy it, and you cannot catch them.", "moral": null},
{"chunk": 310, "story": 310, "summary": "An evergreen olive mocks the fig for shedding leaves, and snow breaks the olive but passes through the fig.", "predicament": "A colleague mocks you for scaling back in lean seasons while they keep up appearances all year round.", "moral": null},
{"chunk": 311, "story": 311, "summary": "The frogs protest the sun's marriage, fearing that more suns would parch them worse.", "predicament": "Someone who already makes your life hard is about to gain more power, and you could protest before it happens.", "moral": null},
{"chunk": 312, "story": 312, "summary": "A brazier scolds his dog for sleeping while he works and waking only to beg at dinner.", "predicament": "Someone who dozes through all the work always wakes up just in time to ask for a share of the rewards.", "moral": null}
]
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Assemble records.json, groups.json and options_by_entry.json from batches/, groups_src.py and
options_src.py, and check them. Jacobs' build.py plus the checks this corpus needs: one scene per
fable, predicament length and name leaks, verbatim morals and evidence, the 13-fable recurrence rule,
and the catalogue ids the options measurement uses. No inference."""
import collections, glob, json, pathlib, re, sys
sys.dont_write_bytecode = True # the catalogue import below must not leave caches outside probe/aesop/
from groups_src import G, STANDING
from options_src import O, NEW_ENTRIES
here = pathlib.Path(__file__).parent
sys.path.insert(0, str(here.parent.parent / 'catalogue' / 'v2'))
from catalogue_v2_src import CATALOGUE
chunks = json.loads((here.parent / 'chunks.json').read_text())
stories = json.loads((here.parent / 'stories.json').read_text())
recs = [r for f in sorted(glob.glob(str(here / 'batches/s*.json'))) for r in json.loads(open(f).read())]
assert [r['chunk'] for r in recs] == list(range(len(chunks))), 'every fable, once, in order'
assert all(chunks[r['chunk']]['story'] == r['story'] for r in recs)
assert all(set(r) == {'chunk', 'story', 'summary', 'predicament', 'moral'} for r in recs)
# ---- records -------------------------------------------------------------------------------------
NAMES = ('amaranth ant ape apple ass bat bear bee beetle bird boar bramble bull calf camel cat cock crab crane '
'crow dog doe dolphin dove eagle elephant fawn fig filbert fir fish flea fly flies frog gnat goat goose '
'geese grape grasshopper hare hart hawk hedgehog heifer hen horse hound jackdaw kid kite lamb lark '
'leopard lion lioness locust mole monkey mouse mice mule nettle nightingale oak olive ox oxen oyster '
'panther partridge peacock pig pigeon pomegranate porker raven reed rose satyr seagull serpent sheep '
'snake sow sparrow sprat stag stork swallow swan thrush tortoise vine viper walnut wasp weasel whale '
'whelp wolf wolves jupiter juno venus mercury minerva neptune momus hercules apollo pallas hermes '
'axe net sling fleece wool manger pitcher honey acorn').split()
NOPLURAL = {'doe'} # 'does' is a verb
leak = re.compile(r'\b(' + '|'.join(n + ('' if n in NOPLURAL else 's?') for n in NAMES) + r')\b', re.I)
def norm(s): return ' '.join(s.split())
problems = []
for r in recs:
p, n = r['predicament'], len(r['predicament'].split())
if not 15 <= n <= 24: problems.append(f"chunk {r['chunk']}: predicament has {n} words")
if len(re.findall(r'[.!?](?:\s|$)', p)) != 1 or not p.endswith('.'): problems.append(f"chunk {r['chunk']}: not one sentence")
if leak.search(p): problems.append(f"chunk {r['chunk']}: name or story object in predicament: {leak.search(p).group(0)}")
if r['moral'] is not None and norm(r['moral']) not in norm(chunks[r['chunk']]['text']):
problems.append(f"chunk {r['chunk']}: moral is not verbatim")
# ---- groups --------------------------------------------------------------------------------------
MIN_MEMBERS = max(3, round(0.04 * len(stories))) # 13 for 313 fables
SKILLS = {'wit', 'prudence'}
seen = collections.Counter(m for g in G for m in g['members'])
dup = [m for m, k in seen.items() if k > 1]
if dup: problems.append(f'fables in two groups: {dup}')
assert set(STANDING) == set(range(len(recs))), 'STANDING covers every fable'
by = {r['chunk']: r for r in recs}
out = []
for g in G:
small = g.get('small', False)
k = len(g['members'])
if small and not 5 <= k <= 12: problems.append(f"{g['id']}: marked small but has {k} fables")
if not small and k < MIN_MEMBERS: problems.append(f"{g['id']}: {k} fables, below the {MIN_MEMBERS}-fable rule")
if not 3 <= len(g['options']) <= 5: problems.append(f"{g['id']}: {len(g['options'])} options")
used = {p for _, _, mv in g['options'] for p in mv if '.' not in p and p not in ('money', 'standing')}
if not used <= SKILLS: problems.append(f"{g['id']}: skill labels outside {SKILLS}: {used - SKILLS}")
for _, s, mv in g['options']:
if not all(v in ('+', '-', 'risk', 'set') for v in mv.values()): problems.append(f"{g['id']}: bad direction in {mv}")
if s.startswith('scene:') and not 0 <= int(s.split(':')[1]) < len(recs): problems.append(f"{g['id']}: bad source {s}")
st = sorted({by[m]['story'] for m in g['members']})
stand = collections.Counter(STANDING[by[m]['story']] for m in g['members'])
out.append({
'id': g['id'], 'name': g['name'], 'predicament': g['predicament'], 'small': small,
'sentence': f"A {g['actor']} wants {g['wants']} from {g['counterpart']}.",
'actor': g['actor'], 'wants': g['wants'], 'counterpart': g['counterpart'],
'stories': st, 'story_titles': [stories[s]['title'] for s in st],
'standing': dict(stand), 'stage': stand.most_common(1)[0][0],
'members': [{'chunk': m, 'story': by[m]['story'], 'predicament': by[m]['predicament'],
'moral': by[m]['moral']} for m in g['members']],
'options': [{'text': t, 'source': s, 'moves': mv,
'in_group': (int(s.split(':')[1]) in g['members']) if s.startswith('scene:') else None}
for t, s, mv in g['options']],
})
unassigned = [r['chunk'] for r in recs if r['chunk'] not in seen]
# ---- options measurement -------------------------------------------------------------------------
v2 = {e['id'] for e in CATALOGUE}
obe, new_used = collections.defaultdict(list), set()
for entry, c, text, mv, ev, stated in O:
if entry.startswith('new:'):
if entry[4:] not in NEW_ENTRIES: problems.append(f'{entry}: not defined in NEW_ENTRIES')
if entry[4:] in v2: problems.append(f'{entry}: already a v2 id')
new_used.add(entry[4:])
elif entry not in v2: problems.append(f'{entry}: not a v2 catalogue id')
ev = by[c]['moral'] if ev == 'M' else ev
if ev is None: problems.append(f'chunk {c}: evidence M but the fable has no moral')
elif norm(ev) not in norm(chunks[c]['text']): problems.append(f'chunk {c}: evidence not verbatim: {ev[:50]}')
elif len(ev.split()) > 30: problems.append(f'chunk {c}: evidence over 30 words')
bad = [p for p in mv if not (p in ('money', 'standing') or p.startswith('relationships.'))]
if bad: problems.append(f'chunk {c}: measurement moves use {bad}; money, standing, relationships only')
if not all(v in ('+', '-', 'risk') for v in mv.values()): problems.append(f'chunk {c}: bad direction in {mv}')
obe[entry].append({'text': text, 'moves': mv, 'source': f'chunk:{c}', 'evidence': ev, 'stated': stated})
unused = sorted(set(NEW_ENTRIES) - new_used)
if unused: problems.append(f'NEW_ENTRIES never used: {unused}')
if problems:
print('\n'.join(problems)); sys.exit(1)
obe_out = {k: obe[k] for k in sorted(obe, key=lambda k: (k.startswith('new:'), k))}
obe_out['_new_entries'] = {f'new:{k}': {'name': n, 'definition': d, 'differs_from': x}
for k, (n, d, x) in NEW_ENTRIES.items()}
(here / 'records.json').write_text(json.dumps(recs, indent=1, ensure_ascii=False))
(here / 'groups.json').write_text(json.dumps({'groups': out, 'unassigned': unassigned}, indent=1, ensure_ascii=False))
(here / 'options_by_entry.json').write_text(json.dumps(obe_out, indent=1, ensure_ascii=False))
# ---- report --------------------------------------------------------------------------------------
big = [g for g in out if not g['small']]
print(f'{len(recs)} fables, {sum(r["moral"] is not None for r in recs)} with a stated moral; '
f'{len(big)} groups of {MIN_MEMBERS}+ fables, {len(out) - len(big)} small groups (5-12); '
f'{sum(seen.values())} assigned, {len(unassigned)} unassigned')
for g in out:
print(f"{g['id']} {len(g['members']):2} fables stage {g['stage']:6} {'SMALL ' if g['small'] else ''}{g['name']}")
src = collections.Counter(o['source'].split(':')[0] for g in out for o in g['options'])
print('group option sources:', dict(src), '| scene options citing a fable outside their group:',
sum(o['in_group'] is False for g in out for o in g['options']))
opts = [o for k, v in obe.items() for o in v]
cov_v2 = sorted(k for k in obe if not k.startswith('new:'))
print(f'options measurement: {len(opts)} options, {sum(o["stated"] for o in opts)} stated '
f'({sum(o["stated"] for o in opts) / len(opts):.0%}); {len(cov_v2)} of {len(v2)} v2 entries covered, '
f'{len(new_used)} new entries; from {len({o["source"] for o in opts})} distinct fables')
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,228 @@
"""Hand-built reference grouping of the Aesop predicaments (Claude-written, no inference of any kind).
One fable is one story and one scene, so a group's member count is also its story count.
Recurrence rule, scaled to the corpus: with 313 fables a group needs at least 13 members
(max(3, 4% of 313) = 13). Groups of 5-12 are allowed but are marked SMALL below and reported
separately. Each fable is in at most one group; fables that fit no recurring situation stay
unassigned.
Options follow DESIGN.md: 3-5 per group, each marked by where it came from --
'archetype' a fixed, setting-free option
'scene:N' what someone in fable N actually did (or refused to do)
'filled' written to fill a gap the fables leave
`moves` names spine paths and a direction only (+ / - / risk / set). No magnitudes: the model never
chooses a number. Skill labels proposed for this corpus: wit (seeing through a trick, and turning it),
prudence (looking before you leap, knowing your limits, keeping what you have).
"""
G = [
# ---- the powerful and the weak ---------------------------------------------------------------------
dict(id='G01', name='A friendly approach from someone who would gain by your fall',
predicament='Someone whose nature is to prey on people like you comes friendly, praising, inviting or offering peace, and what they propose would put you in reach.',
actor='person with something to lose', wants='to keep it', counterpart='a known predator acting friendly',
members=[30, 36, 63, 64, 73, 92, 96, 144, 172, 192, 204, 207, 215, 219, 221, 225],
options=[
('Keep your distance and decline politely.', 'scene:36', {'prudence':'+', 'relationships.predator':'-'}),
('Accept the invitation and go in.', 'scene:221', {'relationships.predator':'+', 'standing':'risk'}),
('Say out loud what they are really after.', 'scene:225', {'wit':'+', 'relationships.predator':'-'}),
('Send them round to someone who can deal with them.', 'scene:215', {'wit':'+', 'relationships.predator':'-', 'flags.turned_the_tables':'set'}),
('Rise to the praise and show them what you can do.', 'scene:96', {'money':'-', 'standing':'-'})]),
dict(id='G02', name='At the mercy of someone who has already decided',
predicament='Someone who holds your fate has caught you and is looking for a reason, and you have one chance to talk your way out.',
actor='person caught', wants='to be let go', counterpart='the one who has them',
members=[1, 5, 26, 39, 43, 55, 131, 135, 147, 197, 211, 217, 248, 292],
options=[
('Answer every charge with the facts.', 'scene:1', {'standing':'risk', 'relationships.captor':'-'}),
('Show them the one thing you do that nobody else can.', 'scene:135', {'standing':'+', 'relationships.captor':'+'}),
('Tell them you belong to the side they spare.', 'scene:5', {'wit':'+', 'flags.two_faced':'set'}),
('Ask for one small favour that puts them off guard.', 'scene:217', {'wit':'+', 'relationships.captor':'-'}),
('Offer to hand over your own people instead.', 'scene:292', {'relationships.colleagues':'-', 'standing':'-', 'flags.informer':'set'})]),
dict(id='G03', name='Trading one hardship for another',
predicament='Your present lot is hard, and you could change master, protector or post, though the new one may be worse.',
actor='person with a hard lot', wants='something better', counterpart='the master or post on offer',
members=[47, 53, 76, 79, 109, 115, 125, 136, 177, 178, 189, 199, 228, 291],
options=[
('Ask for a new master.', 'scene:125', {'relationships.boss':'-', 'standing':'risk'}),
('Bring in a strong protector.', 'scene:79', {'relationships.protector':'+', 'standing':'risk'}),
('Keep the harm you know.', 'scene:53', {'prudence':'+'}),
('Go back to your plain, safe post.', 'scene:228', {'prudence':'+', 'money':'-'}),
('Sell up and stake it all on the change.', 'scene:109', {'money':'risk', 'flags.staked_all':'set'})]),
dict(id='G04', name='Envying another\'s gift',
predicament='You envy a colleague\'s gift or standing so much that you are tempted to copy them, or ask for what they have.',
actor='person with plainer gifts', wants='what the admired one has', counterpart='the admired colleague',
members=[2, 29, 31, 52, 68, 98, 134, 170, 180, 186, 209, 258, 265, 268, 282, 287, 297],
options=[
('Copy what they do.', 'scene:265', {'standing':'risk', 'relationships.admired':'-'}),
('Ask those above for what they have.', 'scene:297', {'relationships.boss':'-', 'standing':'-'}),
('Stick to what you are good at.', 'scene:209', {'prudence':'+'}),
('Look at what their position costs them.', 'scene:287', {'prudence':'+', 'relationships.admired':'+'}),
('Learn the craft properly before you try it.', 'filled', {'money':'-', 'wit':'+'})]),
dict(id='G05', name='Passing for more than you are',
predicament='You are passing for braver, grander or more skilled than you are, and the moment that would test it has come.',
actor='pretender', wants='to keep the credit', counterpart='whoever might test them',
members=[40, 42, 65, 138, 148, 160, 187, 198, 200, 201, 234, 251, 270, 302],
options=[
('Keep up the pretence and say nothing.', 'scene:200', {'standing':'risk'}),
('Confess before you are tested.', 'scene:234', {'standing':'-', 'relationships.examiner':'+'}),
('Laugh with them when it slips.', 'scene:160', {'standing':'+', 'relationships.companions':'+'}),
('Double the claim.', 'scene:198', {'standing':'risk', 'flags.liar':'set'}),
('Back out quietly.', 'scene:138', {'standing':'-'})]),
dict(id='G06', name='Riding high',
predicament='Things are going your way, and you are tempted to show it, crow about it, or take on someone bigger.',
actor='person on top for now', wants='to be seen on top', counterpart='rivals and onlookers',
members=[27, 57, 71, 108, 110, 117, 126, 141, 194, 245, 255, 264, 290],
options=[
('Crow about it where everyone can see.', 'scene:71', {'standing':'risk', 'relationships.rival':'-'}),
('Press on and take on someone bigger.', 'scene:110', {'standing':'risk', 'flags.overreached':'set'}),
('Keep quiet and go on shining.', 'scene:141', {'prudence':'+', 'standing':'+'}),
('Ask an old hand what your mark really means.', 'scene:57', {'relationships.mentor':'+', 'prudence':'+'})]),
dict(id='G07', name='Sneered at by someone showier',
predicament='A showier or better-placed colleague sneers at your plainness or your load, and you must decide how to answer.',
actor='plain worker', wants='respect', counterpart='a showier colleague',
members=[7, 11, 22, 24, 70, 88, 155, 163, 167, 223, 226, 253, 310],
options=[
('Answer with what you can do that they cannot.', 'scene:167', {'wit':'+', 'standing':'+', 'relationships.rival':'-'}),
('Hold your peace and wait.', 'scene:223', {'prudence':'+', 'relationships.rival':'-'}),
('Challenge them to a test.', 'scene:11', {'standing':'risk', 'relationships.rival':'-'}),
('Ignore them and heed only the one in charge.', 'scene:226', {'relationships.boss':'+'}),
('Try to outshine them on their terms.', 'filled', {'money':'-', 'standing':'risk'})]),
# ---- help, greed, partners -------------------------------------------------------------------------
dict(id='G08', name='Help asked in need',
predicament='Someone in trouble asks for help, or you need it yourself, and nobody can tell how the help will be repaid.',
actor='person asked for help', wants='to do right without being bitten', counterpart='someone in need',
members=[0, 10, 23, 51, 132, 154, 190, 193, 208, 230, 261, 286, 295, 296],
options=[
('Help, though they can give nothing back now.', 'scene:0', {'relationships.helped':'+', 'money':'-'}),
('Refuse: they should have looked ahead.', 'scene:10', {'relationships.asker':'-', 'money':'+'}),
('Take them in and nurse them.', 'scene:23', {'relationships.helped':'+', 'standing':'risk'}),
('Refuse to lift any of their load.', 'scene:51', {'relationships.partner':'-', 'money':'risk'}),
('Stick with those who already serve you without promises.', 'scene:230', {'prudence':'+', 'relationships.staff':'+'})]),
dict(id='G09', name='Wanting more than you hold',
predicament='You already have something good, and a chance to grab more, squeeze more or stay longer is tempting you.',
actor='person with something in hand', wants='more', counterpart='the bigger prize',
members=[15, 21, 46, 95, 113, 137, 139, 151, 156, 168, 179, 213, 279],
options=[
('Keep what you hold.', 'scene:137', {'money':'+', 'prudence':'+'}),
('Drop it and chase the bigger prize.', 'scene:156', {'money':'risk'}),
('Squeeze the steady source for more.', 'scene:168', {'money':'risk', 'flags.killed_the_goose':'set'}),
('Take half and get free.', 'scene:46', {'prudence':'+', 'money':'-'}),
('Stay and enjoy it a while longer.', 'scene:279', {'money':'+', 'standing':'risk'})]),
dict(id='G10', name='Your side divided',
predicament='You and a rival, or the people on your own side, are quarrelling, and someone else stands to profit from it.',
actor='member of a divided side', wants='to win the quarrel, or end it', counterpart='the rival, and whoever is waiting',
members=[4, 25, 48, 75, 80, 105, 124, 152, 165, 166, 171, 185, 224, 236, 246],
options=[
('Make peace with the rival before others profit.', 'scene:105', {'relationships.rival':'+'}),
('Show them together they cannot be broken.', 'scene:4', {'relationships.team':'+', 'standing':'+'}),
('Fight it out to the end.', 'scene:152', {'relationships.rival':'-', 'money':'-'}),
('Stop supplying the part you resent.', 'scene:75', {'relationships.team':'-', 'money':'-'}),
('Keep switching to whichever side is winning.', 'scene:246', {'wit':'+', 'relationships.team':'-', 'flags.turncoat':'set'})]),
dict(id='G11', name='A partner you may not be able to count on',
predicament='You are joined, or about to be joined, to a partner or ally, and you cannot be sure they will stand by you.',
actor='person choosing a partner', wants='someone to rely on', counterpart='a doubtful partner',
members=[12, 20, 37, 58, 60, 82, 90, 102, 120, 127, 181, 182, 184, 254, 256, 277, 284, 299],
options=[
('Ask for a surety before you join.', 'scene:277', {'prudence':'+', 'relationships.partner':'-'}),
('Decline: their ways would undo yours.', 'scene:12', {'prudence':'+', 'relationships.partner':'-'}),
('Save yourself and leave the partner to it.', 'scene:37', {'relationships.partner':'-', 'flags.deserted':'set'}),
('Bind yourself to them.', 'scene:90', {'relationships.partner':'+', 'standing':'risk'}),
('Hold them to their own words.', 'scene:120', {'wit':'+', 'relationships.partner':'-'})]),
dict(id='G12', name='Rushing in without looking',
predicament='Something you want is right in front of you, or you are intent on a target, and you have not looked at the danger.',
actor='eager person', wants='what is in front of them', counterpart='the danger they have not seen',
members=[13, 18, 32, 62, 99, 107, 140, 150, 216, 222, 247, 289, 308],
options=[
('Go for it now before it is gone.', 'scene:32', {'money':'risk', 'standing':'risk'}),
('Ask how you get out before going in.', 'scene:140', {'prudence':'+'}),
('Keep your eyes on your target.', 'scene:222', {'money':'+', 'standing':'risk'}),
('Wait for a surer sign before you act.', 'filled', {'prudence':'+', 'money':'-'})]),
dict(id='G13', name='Pointing at others\' faults',
predicament='Faults are being pointed out, by you or at you, and the one pointing shares the fault or gains by the blame.',
actor='critic or criticised', wants='to be in the right', counterpart='the other party',
members=[9, 28, 38, 84, 111, 116, 122, 153, 173, 188, 239, 257, 260, 306],
options=[
('Ask them to show you how.', 'scene:84', {'wit':'+', 'relationships.critic':'-'}),
('Point out that they do the same.', 'scene:122', {'wit':'+', 'relationships.critic':'-', 'standing':'+'}),
('Ask what they gain by the advice.', 'scene:38', {'wit':'+', 'relationships.adviser':'-'}),
('Find fault with everyone.', 'scene:257', {'standing':'-', 'relationships.colleagues':'-'}),
('Look at your own faults first.', 'filled', {'prudence':'+', 'relationships.colleagues':'+'})]),
dict(id='G14', name='Liberties between the weak and the strong',
predicament='Someone powerful is down, caged or out of reach, or someone small is needling you, and a blow could be struck.',
actor='weak or strong party', wants='to strike, or to be left alone', counterpart='the other party',
members=[49, 66, 77, 121, 145, 159, 175, 206, 232, 238, 252, 285, 298, 303, 305],
options=[
('Taunt them while they cannot reach you.', 'scene:66', {'standing':'+', 'relationships.powerful':'-'}),
('Pursue them however long it takes.', 'scene:303', {'relationships.powerful':'-', 'flags.revenge':'set', 'standing':'risk'}),
('Help them while they are down.', 'scene:298', {'relationships.powerful':'+', 'money':'-'}),
('Kick them now that they are down.', 'scene:232', {'relationships.powerful':'-', 'standing':'-'}),
('Let a small insult pass.', 'filled', {'prudence':'+'})]),
dict(id='G15', name='A hard lot',
predicament='You are stuck, worn down or beaten, and it is tempting to despair, complain, or wait for someone to rescue you.',
actor='person in hard times', wants='a way through', counterpart='their own circumstances',
members=[16, 61, 72, 87, 133, 157, 169, 191, 195, 227, 267, 273, 288, 301],
options=[
('Put your own shoulder to it.', 'scene:16', {'prudence':'+', 'money':'+'}),
('Try something nobody has tried.', 'scene:191', {'wit':'+', 'money':'+'}),
('Say you never wanted it anyway.', 'scene:195', {'standing':'+'}),
('Call on your backer.', 'scene:273', {'relationships.patron':'risk'}),
('Give it up entirely.', 'scene:87', {'standing':'-', 'flags.gave_up':'set'})]),
dict(id='G16', name='Worked hard, badly repaid',
predicament='You carry the load for the one you serve, and they take you for granted, work you into the ground or skim what you need.',
actor='worker', wants='fair treatment', counterpart='the one they serve',
members=[34, 45, 50, 74, 100, 118, 119, 142, 161, 196, 271, 283, 312],
options=[
('Carry on in silence.', 'scene:34', {'relationships.boss':'+', 'standing':'-'}),
('Tell them plainly what they are doing to you.', 'scene:50', {'relationships.boss':'risk', 'standing':'+'}),
('Remind them what you were.', 'scene:119', {'relationships.boss':'risk'}),
('Leave before you are next.', 'scene:118', {'prudence':'+', 'money':'-', 'relationships.boss':'-'}),
('Stop being so useful.', 'filled', {'money':'-', 'relationships.boss':'-'})]),
# ---- SMALL groups (5-12 fables): below the 13-fable recurrence rule, reported separately ----------
dict(id='G17', name='Letting a known predator inside', small=True, # SMALL: 6 fables
predicament='Someone with a predator\'s instincts is about to be let inside, or trained, or left in charge, and you know what they are.',
actor='keeper', wants='to use them safely', counterpart='a known predator',
members=[83, 129, 164, 210, 233, 281],
options=[
('Speak up before they are let in.', 'scene:164', {'relationships.boss':'risk', 'standing':'+'}),
('Trust them after long good behaviour.', 'scene:210', {'relationships.newcomer':'+', 'money':'risk'}),
('Let them go and cut your losses.', 'scene:129', {'money':'-', 'prudence':'+'}),
('Train them to your uses.', 'scene:83', {'money':'+', 'relationships.newcomer':'risk'})]),
dict(id='G18', name='The lion\'s share', small=True, # SMALL: 6 fables
predicament='You did the work alongside someone far stronger, and now the proceeds are being divided on their terms.',
actor='junior partner', wants='a fair share', counterpart='a far stronger partner',
members=[3, 101, 130, 158, 274, 309],
options=[
('Divide it fairly.', 'scene:158', {'relationships.partner':'-', 'standing':'risk'}),
('Leave yourself almost nothing.', 'scene:158', {'money':'-', 'relationships.partner':'+'}),
('Go it alone next time.', 'scene:274', {'money':'risk', 'relationships.partner':'-'}),
('Ask for what you were promised.', 'scene:3', {'relationships.partner':'-', 'standing':'risk'})]),
dict(id='G19', name='A bad record when you need trust', small=True, # SMALL: 5 fables
predicament='You need people to believe you or help you now, and your record gives them every reason not to.',
actor='person with a past', wants='to be believed', counterpart='those who remember',
members=[54, 104, 229, 235, 243],
options=[
('Raise the alarm anyway.', 'scene:54', {'standing':'-'}),
('Make a new promise to a new patron.', 'scene:243', {'relationships.patron':'-'}),
('Do a good turn to mend your name.', 'scene:235', {'relationships.neighbour':'risk'}),
('Earn trust in good times, before you need it.', 'filled', {'relationships.allies':'+'})]),
dict(id='G20', name='A warning before the trouble', small=True, # SMALL: 6 fables
predicament='Trouble is coming but not yet here, and you must decide whether to prepare, warn others, or take the threat seriously.',
actor='person with warning', wants='to be ready', counterpart='the trouble coming',
members=[35, 112, 174, 214, 241, 249],
options=[
('Prepare now while it is quiet.', 'scene:112', {'prudence':'+', 'money':'-'}),
('Warn the others though they laugh.', 'scene:249', {'standing':'-', 'relationships.team':'-'}),
('Wait until they mean it.', 'scene:214', {'prudence':'+'}),
('Put it off until next time.', 'scene:241', {'money':'+', 'prudence':'-'})]),
]
# Protagonist's power in the situation each predicament describes, for stage assignment (DESIGN.md
# "Stages"). One fable is one scene, so this is per fable. 'high' means the "you" of the predicament
# holds power over the others in it, 'middle' that they deal as a peer, 'low' that they are at
# someone's mercy or under their orders.
HIGH = {0, 4, 17, 33, 49, 69, 73, 84, 85, 86, 89, 126, 137, 152, 153, 156, 162, 173, 175, 183, 202,
203, 213, 220, 229, 230, 231, 257, 261, 269, 271, 277, 278, 288, 293, 305, 312}
MIDDLE = {2, 6, 7, 10, 12, 14, 15, 21, 22, 23, 24, 25, 27, 28, 30, 31, 37, 38, 40, 42, 44, 46, 48, 51,
58, 59, 60, 61, 62, 65, 67, 71, 78, 83, 91, 93, 95, 96, 97, 102, 103, 105, 108, 111, 112, 114,
116, 120, 123, 124, 129, 132, 138, 139, 140, 141, 143, 148, 149, 150, 151, 154, 155, 159,
160, 163, 165, 167, 168, 171, 174, 179, 180, 182, 184, 185, 186, 188, 190, 192, 194, 199,
201, 209, 210, 212, 219, 222, 224, 225, 227, 233, 235, 236, 239, 242, 244, 245, 249, 250,
251, 252, 256, 260, 262, 263, 270, 272, 281, 282, 289, 290, 296, 297, 298, 299, 302, 304, 310}
STANDING = {i: 'high' if i in HIGH else 'middle' if i in MIDDLE else 'low' for i in range(313)}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,342 @@
"""The options measurement: which catalogue situations the fables supply options for, and whether the
fable itself shows where each option's moves go. Hand-written by Claude reading every fable; no inference.
Each row: (entry, chunk, text, moves, evidence, stated)
entry a catalogue v2 id, or 'new:<id>' defined in NEW_ENTRIES
chunk the fable the option comes from (written out as source 'chunk:N')
text the move, transplanted to a workplace
moves spine paths with a direction. Only money, standing and relationships.<role>: a skill label
or a flag is a design decision about the option, not an outcome a story can show, so
none is used here (the group options in groups_src.py carry them)
evidence a verbatim excerpt of the fable, at most 30 words, or 'M' for the fable's stated moral
stated True only if the fable's own outcome or moral shows the direction of EVERY path in moves.
How an outcome is read as a direction (the same reading for every row):
money goods, food, pay, property or a prize gained (+) or lost (-); a heavier load or harder
work for the same keep counts as (-)
standing the actor's safety, position or name: made king, spared, pardoned, praised, winning
the contest (+); killed, caught, beaten, driven out, exposed or publicly rebuked (-)
relationships.<role> how that party treats the actor afterwards: repays, rescues, makes peace (+);
turns on them, deserts them, refuses them, stops trusting them (-)
'risk' stated only when the same fable shows the same move ending both ways
A moral that only calls a move wise or foolish does not state a direction; one that names the
effect ("wins the race", "lost both") does. An outcome that befalls the actor but is not caused by
the move (the sparrow taken by a hawk while mocking the hare) is not counted as stated.
"""
NEW_ENTRIES = {
'friendly_predator': ('A friendly approach from someone who would gain by your fall',
'Someone whose nature is to prey on people like you comes friendly, praising, inviting, offering peace or a small favour to ask, and what they propose would put you in their reach.',
'near sure_thing (an easy fortune that is a trap) and not_who_they_seem (a false face among you); here the danger is known, and the bait is friendship, not fortune'),
'better_the_devil': ('Trading one hardship for another',
'Your present master, protector or post is hard on you, and you could change it, though the new one may be worse.',
'no v2 entry; the decision is whether to change your lot, not how to bear it'),
'ape_your_betters': ('Envying another\'s gift',
'You envy a colleague\'s gift, looks or standing so much that you are tempted to copy them, ask for what they have, or leave your own kind to join theirs.',
'near overlooked (wanting, once, to be noticed beside the admired one); here the move is imitation'),
'riding_high': ('Riding high',
'Things are going your way, and you are tempted to show it: crow, parade your honours, or take on someone bigger.',
'no v2 entry; the fables\' most common turn, pride before a fall'),
'sneered_at': ('Sneered at by someone showier',
'A showier or better-placed colleague sneers at your plainness, slowness or load; you are the one sneered at, or the one sneering.',
'near power_over_you and despised, but the sneerer has no power over you'),
'grasping': ('Wanting more than you hold',
'You already have something good, and the chance to grab more, squeeze more out of it, or stay longer is tempting you.',
'no v2 entry'),
'divided': ('Your side divided',
'You and a rival, or the people on your own side, are quarrelling, and someone else stands to profit from it.',
'no v2 entry'),
'doubtful_ally': ('A partner you may not be able to count on',
'You are joined, or about to be joined, to a partner or ally, and cannot be sure they will stand by you.',
'near used; here nothing is hidden yet, the question is whether to rely on them'),
'heedless': ('Rushing in without looking',
'Something you want is right in front of you, or you are intent on a target, and you have not looked at the danger.',
'no v2 entry'),
'pot_and_kettle': ('Pointing at others\' faults',
'Faults are being pointed out, by you or at you, and the one pointing shares the fault or gains by the blame.',
'near own_words_back'),
'needling_the_strong': ('Liberties between the weak and the strong',
'Someone powerful is down, caged or out of reach, or someone small is needling you, and a blow could be struck.',
'near revenge and at_their_mercy'),
'hard_lot': ('A hard lot',
'You are stuck, worn down or beaten, and it is tempting to despair, complain, or wait for someone to rescue you.',
'near dead_end, which is years of thankless work; this is any hardship'),
'used_up': ('Worked hard, badly repaid',
'You carry the load for the one you serve, and they take you for granted, work you into the ground or skim what you need.',
'near overlooked and bow_for_your_living'),
'predator_inside': ('Letting a known predator inside',
'Someone with a predator\'s instincts is about to be let in, trained or left in charge, and you know what they are.',
'near warning'),
'lions_share': ('The lion\'s share',
'You did the work alongside someone far stronger, and the proceeds are being divided on their terms.',
'near used and power_over_you'),
'bad_record': ('A bad record when you need trust',
'You need people to believe or help you now, and your record gives them every reason not to.',
'near unbelievable_truth, but here the listeners are right to doubt you'),
'asking_above': ('Asking the power above for a favour',
'You can ask someone far above you for a favour, a change or a rescue, and what you ask for may not serve you.',
'near patron and benefactor; the fables\' petitions to Jupiter'),
'please_everyone': ('Trying to please everyone',
'Several people want different things of you, and changing to suit each one in turn is tempting.',
'near peace_at_home'),
'make_them_move': ('Getting someone to move',
'You need someone to do or give up something, and must choose between coaxing, tricking and forcing them.',
'no v2 entry'),
}
M = 'M'
O = [
# ---- at the mercy of the powerful ----------------------------------------------------------------
('at_their_mercy', 1, 'Answer every charge with the facts.', {'standing':'-'}, M, True),
('at_their_mercy', 5, 'Tell each captor you belong to the side they spare.', {'standing':'+'}, 'The Bat assured him that he was not a mouse, but a bat, and thus a second time escaped.', True),
('at_their_mercy', 26, 'Plead your good character, though you were caught with the wrongdoers.', {'standing':'-'}, 'I have taken you with these robbers, the Cranes, and you must die in their company.', True),
('at_their_mercy', 39, 'Defend yourself against the excuse they have found.', {'standing':'-'}, 'Although you abound in specious apologies, I shall not remain supperless;', True),
('at_their_mercy', 55, 'Ask them to stop, since their sport is your ruin.', {'relationships.powerful':'+'}, 'Pray stop, my boys: what is sport to you, is death to us.', False),
('at_their_mercy', 131, 'Plead how useful you are when they must cut someone.', {'standing':'risk'}, 'The Birdcatcher spared his life, and determined to pick out a fine young Cock just attaining to his comb.', True),
('at_their_mercy', 135, 'Show the one real talent that sets you apart.', {'standing':'+'}, 'made himself known by his voice, and preserved his life by his melody.', True),
('at_their_mercy', 147, 'Plead your usefulness to people your usefulness threatens.', {'standing':'-'}, 'That is the very reason why we must the more kill you,', True),
('at_their_mercy', 197, 'Ask the one who has you for a small favour that may draw help.', {'standing':'+'}, 'some hounds hearing the sound ran up and began chasing the Wolf.', True),
('at_their_mercy', 211, 'Argue that everyone should be equal, with nothing to back it.', {'standing':'-'}, 'Your words, O Hares! are good; but they lack both claws and teeth such as we have.', False),
('at_their_mercy', 217, 'Ask for a small service that puts them off guard.', {'standing':'+', 'relationships.captor':'-'}, 'the Ass, with his heels, kicked his teeth into his mouth and galloped away.', True),
('at_their_mercy', 248, 'Plead that you never did harm yourself, you only urged others on.', {'standing':'-'}, 'That is the very reason for which you should be put to death,', True),
('at_their_mercy', 292, 'Offer to deliver your colleagues in return for your own escape.', {'standing':'-'}, 'I shall now with less scruple take your life, because you are willing to save it at the cost of betraying your friends and relations.', True),
('at_their_mercy', 293, 'Plead that your offence was too small to matter.', {'standing':'-'}, 'Now you shall certainly die by mine own hands, for no evil, whether it be small or large, ought to be tolerated.', True),
('station', 43, 'Agree to the powerful suitor, on a condition that leaves them harmless.', {'relationships.powerful':'-'}, 'the Woodman, no longer afraid, set upon him with his club, and drove him away into the forest.', True),
('power_over_you', 205, 'Give a vain leader your honest opinion.', {'standing':'-'}, 'The King of the Apes, enraged at hearing these truths, gave him over to the teeth and claws of his companions.', True),
('power_over_you', 205, 'Flatter a vain leader.', {'money':'+'}, 'The Ape and all his court, gratified with the lie, commanded that a handsome present be given to the flatterer.', True),
# ---- a friendly approach from a predator ----------------------------------------------------------
('new:friendly_predator', 36, 'Stay outside and ask after them from a safe distance.', {'relationships.predator':'-'}, 'I notice that there are many prints of feet entering your cave, but I see no trace of any returning.', False),
('new:friendly_predator', 36, 'Go in when they call you close.', {'standing':'-'}, 'The beasts expressed their sorrow, and came one by one to his den, where the Lion devoured them.', True),
('new:friendly_predator', 63, 'Dismiss your protectors to make peace with your enemies.', {'standing':'-'}, 'The Sheep, poor silly creatures, were easily beguiled and dismissed the Dogs, whereupon the Wolves destroyed the unguarded flock at their own pleasure.', True),
('new:friendly_predator', 64, 'Tell the helper you will stay well if they go away.', {'relationships.predator':'-'}, 'We are all very well, and shall continue so, if you will only be good enough to go away, and leave us as we are.', False),
('new:friendly_predator', 92, 'Refuse the small favour that would bring you in reach.', {'relationships.predator':'-'}, M, False),
('new:friendly_predator', 96, 'Speak up to prove the flatterer\'s one doubt wrong.', {'money':'-', 'standing':'-'}, 'My good Crow, your voice is right enough, but your wit is wanting.', True),
('new:friendly_predator', 144, 'Stay put, however harmless they now look.', {'relationships.predator':'-'}, 'even though you should turn into a meal-bag, we will not come near you.', False),
('new:friendly_predator', 192, 'Act at once on a stranger\'s alarming story.', {'money':'-'}, 'The Thief made off with the coat and did not return again to the inn.', True),
('new:friendly_predator', 207, 'Go for the tempting thing lying in the dark corner.', {'standing':'-'}, 'A Mouse, supposing him to be food, leaped upon him, and was instantly caught and squeezed to death.', True),
('new:friendly_predator', 215, 'Send the flatterer round to the one who guards you.', {'relationships.predator':'-'}, 'When the Fox approached the tree, the Dog sprang out and caught him, and tore him to pieces.', True),
('new:friendly_predator', 219, 'Look at the preparations and leave without a word.', {'relationships.host':'-'}, 'The Lion inquired why he went off so abruptly without a word of salutation to his host', True),
('new:friendly_predator', 221, 'Accept the sudden praise and invitation of someone you annoyed.', {'standing':'-'}, 'The Owl came forth from her hollow, seized her, and put her to death.', True),
('new:friendly_predator', 225, 'Stay on your secure ledge and name what they are after.', {'relationships.predator':'-'}, 'it is not for the pasture that you invite me, but for yourself, who are in want of food.', False),
('in_your_keeping', 204, 'Refuse a stranger\'s sudden favours and keep watch.', {'relationships.stranger':'-'}, 'This sudden kindness at your hands will only make me more watchful', False),
('sure_thing', 73, 'Take up the windfall a jealous colleague says they kept for you.', {'standing':'-'}, 'The Monkey approached carelessly and was caught in the trap;', True),
('used', 30, 'Let the one in trouble climb out on your back, on a promise to pull you after.', {'relationships.partner':'-', 'standing':'-'}, 'he safely reached the mouth of the well and made off as fast as he could.', True),
('used', 309, 'Hand your catch to someone who says they will pay for it.', {'money':'-'}, 'However, when the Horseman got the hare, he rode off as fast as he could.', True),
('your_scheme', 185, 'Warn each neighbour privately that the other plots against them.', {'money':'+'}, 'afforded ample provision for the Cat and her kittens.', True),
('your_scheme', 56, 'Repeat the shortcut that lightened your load once.', {'money':'-'}, 'And thus his trick recoiled on him, for he now carried on his back a double burden.', True),
('your_scheme', 218, 'Out of envy, urge a colleague to fake an illness to escape work.', {'standing':'-'}, 'They at once killed the Goat, and so healed the Ass.', True),
('compromise_for_money', 172, 'Hand over what you guard, for a share with the rivals.', {'standing':'-'}, 'entering the den of the Wolves, they were set upon and torn to pieces.', True),
('compromise_for_money', 268, 'Tell the truth about what you lost, though a richer answer is offered.', {'money':'+'}, 'Mercury, pleased with his honesty, gave him the golden and silver axes in addition to his own.', True),
('compromise_for_money', 268, 'Claim the richer thing is yours.', {'money':'-'}, 'not only took away the golden axe, but refused to recover for him the axe he had thrown into the pool.', True),
('pressed_to_wrong', 20, 'Hand your partner over to the powerful in return for your own safety.', {'standing':'-', 'relationships.partner':'-'}, 'The Lion, seeing that the Ass was secured, immediately clutched the Fox, and attacked the Ass at his leisure.', True),
('strings', 199, 'Accept a powerful helper\'s harness to get your revenge.', {'standing':'-'}, 'instead of obtaining revenge on the Stag, he had enslaved himself to the service of man.', True),
('strings', 115, 'Turn down a comfortable post that comes with a chain.', {'money':'-'}, 'May no friend of mine ever be in such a plight; for the weight of this chain is enough to spoil the appetite.', False),
# ---- trading hardships, and asking the power above ------------------------------------------------
('new:better_the_devil', 47, 'Keep demanding a stronger leader from above.', {'standing':'-'}, 'sent a Heron, who preyed upon the Frogs day by day till there were none left to croak upon the lake.', True),
('new:better_the_devil', 53, 'Keep the harm you know rather than risk a worse replacement.', {'standing':'+'}, M, False),
('new:better_the_devil', 76, 'Get rid of what sets the harsh pace.', {'money':'-'}, 'woke them up to their work in the middle of the night.', True),
('new:better_the_devil', 79, 'Invite a powerful protector inside.', {'standing':'-'}, 'he made more havoc and slew a larger number of them in one day than the Kite could pounce upon in a whole year.', True),
('new:better_the_devil', 109, 'Sell what you have and stake it all on a calm-looking trade.', {'money':'-'}, 'he threw all his merchandise overboard, and barely escaped with his life in the empty ship.', True),
('new:better_the_devil', 125, 'Ask for a new master each time you are unhappy.', {'money':'-'}, 'The Ass found that he had fallen into worse hands', True),
('new:better_the_devil', 136, 'Flee into the shelter of someone more dangerous.', {'standing':'-'}, 'when she was safe within the cave, sprang upon her and tore her to pieces.', True),
('new:better_the_devil', 176, 'Leave the field you are made for to chase better prospects.', {'standing':'-'}, 'A Fox came across him, and being very hungry ate him up.', True),
('new:better_the_devil', 177, 'Refuse to fight a takeover, since any master will work you the same.', {'relationships.boss':'-'}, M, False),
('new:better_the_devil', 178, 'Keep the sated drainers rather than invite hungrier ones.', {'money':'+'}, 'others more hungry will come in their place, and will drink up all the blood I have left.', False),
('new:better_the_devil', 189, 'Stay in your familiar, exposed spot.', {'standing':'-'}, 'A few days afterwards a heavy wagon passed through the gully and crushed him to death under its wheels.', True),
('new:better_the_devil', 228, 'Go back to plain fare where you can live without fear.', {'money':'-'}, 'I prefer my bare plowlands and roots from the hedgerow, where I can live in safety, and without fear.', True),
('new:better_the_devil', 291, 'Take refuge with those who may also harm you, rather than with the one chasing you.', {'standing':'+'}, 'It would be better for me to be sacrificed in the Temple than to be eaten by you.', False),
('new:asking_above', 16, 'Wait for someone powerful to rescue you before you try.', {'relationships.patron':'-'}, 'never more pray to me for help, until you have done your best to help yourself', True),
('new:asking_above', 114, 'Ask your patron for the power to punish anyone who touches what is yours.', {'standing':'risk'}, 'if you use your sting, it shall remain in the wound you make, and then you will die from the loss of it.', False),
('new:asking_above', 157, 'Smash your ties with the patron who never helped you.', {'money':'+'}, 'When its head was knocked off, out came a stream of gold, which the Carpenter quickly picked up', True),
('new:asking_above', 297, 'Ask the boss for a colleague\'s distinctions as well as your own.', {'standing':'-'}, 'not only refused to give him horns, but even deprived him of a portion of his ears.', True),
# ---- envy and pretence ----------------------------------------------------------------------------
('new:ape_your_betters', 2, 'Copy the habits of the one you admire.', {'standing':'-'}, 'The Ass resolved that he would live only upon dew, and in a short time died of hunger.', True),
('new:ape_your_betters', 29, 'Pay someone powerful to lift you into a role beyond your nature.', {'standing':'-'}, 'suddenly he let her go, and she fell on a lofty mountain, dashing her shell to pieces.', True),
('new:ape_your_betters', 31, 'Give up the work that feeds you to copy their habits.', {'money':'-'}, 'he could not change their color, while through want of food he perished.', True),
('new:ape_your_betters', 52, 'Try the favourite\'s tricks on the boss.', {'standing':'-'}, 'drove out the Ass to his stable with kicks and clubs and cuffs.', True),
('new:ape_your_betters', 68, 'Puff yourself up to match the one who crushed yours.', {'standing':'-'}, 'you would, I assure you, sooner burst than successfully imitate the hugeness of that monster.', False),
('new:ape_your_betters', 98, 'Copy the antics everyone applauded in a favourite.', {'standing':'-'}, 'The owner went up after him and quickly drove him down, beating him severely with a thick wooden cudgel.', True),
('new:ape_your_betters', 134, 'Try the experts\' job with their tools, untaught.', {'standing':'-'}, 'became tangled in the meshes and drowned.', True),
('new:ape_your_betters', 170, 'Speak up as if your word carried a respected colleague\'s weight.', {'standing':'-'}, M, True),
('new:ape_your_betters', 180, 'Copy an admired style until you lose your own.', {'standing':'-'}, 'in trying to neigh, they forgot how to sing.', False),
('new:ape_your_betters', 186, 'Leave your peers to live among those at the top.', {'standing':'-'}, 'in a herd of lions you are definitely a wolf.', False),
('new:ape_your_betters', 258, 'Try a bigger version of the star performer\'s feat.', {'standing':'-'}, 'He at once clipped the Jackdaw’s wings, and taking him home at night, gave him to his children.', True),
('new:ape_your_betters', 265, 'Get up and try to outdo the colleague everyone applauded.', {'standing':'-'}, 'the Beasts, in a fit of indignation, set upon him with clubs and drove him out of the assembly.', True),
('pretence', 40, 'Disguise yourself as one of the harmless to get what you want.', {'standing':'-'}, 'mistakenly caught up the Wolf instead of a sheep, and killed him instantly.', True),
('pretence', 42, 'Boast of a feat done far away, with witnesses.', {'standing':'-'}, 'Suppose this to be Rhodes, and leap for us.', False),
('pretence', 65, 'Present yourself decked in achievements borrowed from colleagues.', {'standing':'-', 'relationships.colleagues':'-'}, 'each plucked from him his own feathers, leaving the Jackdaw nothing but a Jackdaw.', True),
('pretence', 198, 'Lie about your background to someone helping you as one of their own.', {'standing':'-'}, 'The Dolphin, indignant at these falsehoods, dipped the Monkey under the water and drowned him.', True),
('pretence', 200, 'Disguise yourself to share a better-fed group\'s benefits.', {'standing':'-', 'relationships.own_kind':'-'}, 'They too, not recognizing him on account of his color, expelled him from living with them.', True),
('pretence', 201, 'Inflate your background where nobody can contradict you.', {'standing':'-'}, M, False),
('pretence', 234, 'Confess when your fake expertise is about to be tested.', {'standing':'-'}, 'confessed that he had no knowledge of medicine, and was only made famous by the stupid clamors of the crowd.', True),
('pretence', 251, 'Borrow the trappings of authority and frighten people with them.', {'standing':'-'}, 'I might possibly have been frightened myself, if I had not heard your bray.', True),
('courage_doubted', 138, 'Talk big, then back out when offered the real thing.', {'standing':'-'}, 'it is his track only I am in search of, not the Lion himself.', False),
('courage_doubted', 270, 'Run when your partner is in danger, then boast once it is over.', {'relationships.partner':'-'}, 'no dependence can be placed on your valor.', True),
('habit', 148, 'Let an old appetite break your polished act.', {'standing':'-'}, 'The dancing spectacle thus came to an end amidst the laughter and ridicule of the audience.', True),
('habit', 302, 'Let an old temptation pull you out of your new role.', {'standing':'-'}, 'Venus was much disappointed and again caused her to return to her former shape.', True),
('caught_out', 99, 'Relax once the careless have passed you by.', {'standing':'-'}, 'he ordered that the Stag should be seized and killed.', True),
('caught_out', 160, 'Laugh along when your secret is exposed.', {'relationships.companions':'+'}, 'with great glee joined in the joke', False),
('cover_up_spiral', 41, 'Ask the witness to keep quiet about the damage.', {'relationships.boss':'-'}, 'the horn will speak though I be silent.', False),
# ---- riding high, and the sneer -------------------------------------------------------------------
('new:riding_high', 27, 'Make a great commotion about what you are about to produce.', {'standing':'-'}, 'out came a Mouse.', False),
('new:riding_high', 57, 'Parade the mark you take for an honour.', {'standing':'-'}, 'a public notice to all men to avoid you as an ill mannered dog.', True),
('new:riding_high', 71, 'Crow over your beaten rival from the most exposed place.', {'standing':'-'}, 'An Eagle sailing through the air pounced upon him and carried him off in his talons.', True),
('new:riding_high', 108, 'Wear your marks of rank into the fight.', {'standing':'-'}, 'The generals, not being able to get in on account of the ornaments on their heads, were all captured and eaten by the Weasels.', True),
('new:riding_high', 110, 'Chase someone powerful who has just backed off from another.', {'standing':'-'}, 'the Lion, turning about, seized him and tore him to pieces.', True),
('new:riding_high', 117, 'Take the bows meant for what you carry as your own.', {'standing':'-'}, 'laid his whip lustily about his shoulders', True),
('new:riding_high', 126, 'Ask, incognito, how highly you are rated.', {'standing':'-'}, 'Well, if you will buy these, I’ll fling you that into the bargain.', True),
('new:riding_high', 141, 'Claim to outshine the people who truly lead.', {'standing':'-'}, 'Boast no more, but henceforth be content to give thy light in silence.', True),
('new:riding_high', 194, 'Celebrate loudly after beating a powerful opponent.', {'standing':'-'}, 'he became entangled in the meshes of a cobweb and was eaten by a spider.', False),
('new:riding_high', 245, 'Think yourself bigger than those above you.', {'standing':'-'}, 'this overestimation of myself is the cause of my destruction.', True),
('new:riding_high', 264, 'Make a show of whether to leave, sure you will be missed.', {'standing':'-'}, 'I did not know you had come, and I shall not miss you when you go away.', True),
('new:riding_high', 290, 'Carry your valuable load with your bells ringing.', {'money':'-', 'standing':'-'}, 'wounded with a sword the Mule carrying the treasure, which they greedily seized while taking no notice of the grain.', False),
('new:sneered_at', 7, 'Answer that your plainness lasts when times turn hard.', {'standing':'+'}, 'The Crow put an end to the dispute by saying,', True),
('new:sneered_at', 11, 'Challenge the one who mocks you, and keep a steady pace.', {'standing':'+'}, M, True),
('new:sneered_at', 11, 'Take the contest for granted and rest.', {'standing':'-'}, 'he saw the Tortoise had reached the goal, and was comfortably dozing after her fatigue.', True),
('new:sneered_at', 22, 'Answer that yours are few but of real quality.', {'standing':'+'}, M, False),
('new:sneered_at', 70, 'Mock the hard worker from your idle favour.', {'standing':'-'}, 'For this you were allowed to live in idleness, because you were presently to be sacrificed.', False),
('new:sneered_at', 155, 'Answer the display of credentials with your wits.', {'standing':'+'}, 'And how much more beautiful than you am I, who am decorated, not in body, but in mind.', False),
('new:sneered_at', 167, 'Answer the show-off with what you can actually do.', {'standing':'+'}, M, False),
('new:sneered_at', 223, 'Hold your peace and wait for their fall.', {'relationships.rival':'-'}, 'The Ass held his peace, and made only a silent appeal to the justice of the gods.', False),
('new:sneered_at', 223, 'Sneer at a laden colleague from your height of favour.', {'standing':'-'}, 'the Horse, having become broken-winded, was sent by his owner to the farm.', False),
('new:sneered_at', 226, 'Heed only the one in charge, not the loud one.', {'relationships.boss':'+'}, 'I only care for him who sits above you', False),
('new:sneered_at', 310, 'Keep up appearances in every season, and mock those who scale back.', {'standing':'-'}, 'at once despoiling it of its beauty and killing the tree.', True),
('new:sneered_at', 310, 'Scale back in lean seasons, whatever the mockers say.', {'standing':'+'}, 'the snow fell through to the ground, and did not injure it at all.', False),
# ---- help asked, and who repays -------------------------------------------------------------------
('plea_for_help', 0, 'Spare someone beneath you who begs to be let off.', {'relationships.spared':'+'}, 'now you know that it is possible for even a Mouse to confer benefits on a Lion.', True),
('plea_for_help', 10, 'Refuse someone who idled while you saved.', {'relationships.asker':'-'}, 'If you were foolish enough to sing all the summer, you must dance supperless to bed in the winter.', False),
('plea_for_help', 10, 'Ask those who saved to share with you, having idled yourself.', {'standing':'-'}, 'If you were foolish enough to sing all the summer, you must dance supperless to bed in the winter.', True),
('plea_for_help', 23, 'Take in and nurse someone known for treachery.', {'relationships.helped':'-', 'standing':'-'}, 'bit its benefactor, inflicting on him a mortal wound.', True),
('plea_for_help', 51, 'Refuse to take any of a struggling partner\'s load.', {'relationships.partner':'-', 'money':'-'}, 'I should not now be bearing, together with his burden, himself as well.', True),
('plea_for_help', 132, 'Help someone in trouble who can give nothing now.', {'relationships.helped':'+'}, 'The Ant, perceiving his design, stung him in the foot.', True),
('plea_for_help', 154, 'Free someone caught in a trap.', {'relationships.freed':'+'}, 'he marveled at the service rendered him by the Eagle.', True),
('plea_for_help', 190, 'Rescue someone from a dangerous enemy.', {'relationships.rescued':'+', 'relationships.enemy':'-'}, 'The Serpent, irritated at the escape of his prey, injected his poison into the drinking horn of the countryman.', True),
('plea_for_help', 230, 'Give to those who already serve you, not to those who promise.', {'relationships.staff':'+'}, 'It is surely better for me to give the water to them than to you.', False),
('plea_for_help', 286, 'Grab for help from someone known to take from everyone.', {'standing':'-'}, 'Having pricked and grievously torn the soles of his feet', True),
('plea_for_help', 296, 'Pull the thorn from someone dangerous who is in pain.', {'relationships.helped':'+', 'standing':'+'}, 'the Shepherd to be pardoned and restored to his friends.', True),
('protecting_hunted', 127, 'Protect the hunted in words while your gestures give them away.', {'relationships.hunted':'-'}, 'the Fox departed without taking any notice of the Woodcutter', True),
('protecting_hunted', 193, 'Help yourself to your protector\'s things once the danger seems past.', {'standing':'-'}, 'seeing the Hart, shot an arrow from his bow and struck it.', True),
('despised', 0, 'Offer your help to someone powerful who laughs at the idea.', {'relationships.powerful':'+'}, 'The Lion laughed and let him go.', True),
('unwelcome_in_home', 261, 'Let someone use your space a little longer.', {'money':'-'}, 'asserted her exclusive right to the place and would not permit the shepherd to approach.', True),
('unwelcome_in_home', 294, 'Keep promising the neighbour who wants you gone that you will leave soon.', {'relationships.neighbour':'+'}, 'the rich man became accustomed to the smell, and feeling no manner of inconvenience, made no further complaints.', False),
('after_your_money', 60, 'Let every well-wisher help themselves while you are down.', {'money':'-'}, 'he died, not from his sickness, but from the failure of the means of living.', True),
# ---- wanting more ---------------------------------------------------------------------------------
('new:grasping', 15, 'Let go of what you hold to snatch at a bigger prize.', {'money':'-'}, 'He thus lost both:', True),
('new:grasping', 21, 'Keep indulging in the easy pleasure.', {'standing':'-'}, 'for the sake of a little pleasure we have destroyed ourselves.', True),
('new:grasping', 46, 'Take half and get your hand free.', {'money':'-'}, 'Be satisfied with half the quantity, and you will readily draw out your hand.', False),
('new:grasping', 95, 'Repeat the pleasing exchange every day.', {'relationships.partner':'-'}, 'If you go on in this way, you will soon destroy by frequent use the pleasure of your exchange', False),
('new:grasping', 113, 'Spend, in your head, what your one asset will become.', {'money':'-'}, 'down fell the milk pail to the ground, and all her imaginary schemes perished in a moment.', True),
('new:grasping', 137, 'Keep the small sure catch rather than wait for a bigger one.', {'money':'+'}, 'I were to forego my present certain gain.', True),
('new:grasping', 139, 'Help yourself freely to what is left unguarded.', {'standing':'-'}, 'he was so full that he was not able to get out', False),
('new:grasping', 151, 'Bite off more than you can swallow in someone else\'s field.', {'standing':'-'}, 'burst its deep gullet-bag and lay down on the shore to die.', True),
('new:grasping', 156, 'Leave the sure win to chase the bigger prize.', {'money':'-'}, 'I am rightly served, for having let go of the food that I had in my hand for the chance of obtaining more.', True),
('new:grasping', 168, 'Break open the steady source to get everything at once.', {'money':'-'}, 'deprived themselves of the gain of which they were assured day by day.', True),
('new:grasping', 179, 'Feed the steady source extra to double what it yields.', {'money':'-'}, 'the Hen became fat and sleek, and never once laid another egg.', True),
('new:grasping', 213, 'Keep the one in hand rather than chase bigger ones not yet in sight.', {'money':'+'}, 'I should indeed have lost my senses if I should let go food ready in my hand', True),
('new:grasping', 266, 'Agree on a plan that needs the impossible to reach what you want.', {'standing':'-'}, 'they burst themselves with drinking long before they reached the hides.', True),
('new:grasping', 279, 'Linger where it is most rewarding.', {'standing':'-'}, 'For the sake of a little pleasant food I have deprived myself of my life.', True),
('mistook_them', 44, 'Hoard what you never use.', {'money':'-'}, 'digging down, came to the lump of gold, and stole it.', True),
('mistook_them', 262, 'Prize your showy strengths over your plain ones.', {'standing':'-'}, 'These feet which would have saved me I despised, and I gloried in these antlers which have proved my destruction.', True),
# ---- divided sides and doubtful partners ----------------------------------------------------------
('new:divided', 4, 'Show your quarrelling people that together they cannot be broken.', {'relationships.team':'+'}, 'if you are divided among yourselves, you will be broken as easily as these sticks.', False),
('new:divided', 75, 'Stop supplying the part you resent.', {'standing':'-'}, 'The whole Body quickly became debilitated', True),
('new:divided', 80, 'Offer to settle a quarrel between powerful factions.', {'relationships.factions':'-'}, 'We would far rather be destroyed in our battle with each other than admit any interference from you in our affairs.', True),
('new:divided', 105, 'Make peace with your rival once you see others waiting to profit.', {'relationships.rival':'+'}, 'They at once made up their quarrel', True),
('new:divided', 124, 'Fight a client over a technicality.', {'money':'-'}, 'The quarrel proceeded from words to blows, and while the men fought, the Ass galloped off.', True),
('new:divided', 152, 'Fight a rival to exhaustion over one prize.', {'money':'-'}, 'He ran in between them, and seizing the Kid scampered off as fast as he could.', True),
('new:divided', 171, 'Give up one junior member when you are asked.', {'standing':'-'}, 'The first step has lost us all.', True),
('new:divided', 185, 'Believe a colleague\'s whisper that your neighbour means you harm.', {'relationships.neighbour':'-', 'standing':'-'}, 'the Eagle, full of fear of the Sow, sat still on the branches, and the Sow, terrified by the Eagle, did not dare to go out from her cave.', True),
('new:divided', 224, 'Go your own way when a smooth talker suggests it.', {'standing':'-'}, 'he attacked them without fear as they fed alone, and feasted on them one by one at his own leisure.', True),
('new:divided', 246, 'Keep switching to whichever side is winning.', {'relationships.both_sides':'-', 'standing':'-'}, 'being condemned by each for his treachery, he was driven forth from the light of day', True),
('new:doubtful_ally', 12, 'Decline to share premises with someone whose work undoes yours.', {'money':'-'}, 'whatever I should whiten, you would immediately blacken again with your charcoal.', False),
('new:doubtful_ally', 37, 'Save yourself and leave your companion to face it.', {'relationships.companion':'-'}, 'Never travel with a friend who deserts you at the approach of danger.', True),
('new:doubtful_ally', 58, 'Treat new allies better than old ones to win them.', {'relationships.new_allies':'-'}, 'the Wild Goats scampered away as fast as they could to the mountains.', True),
('new:doubtful_ally', 90, 'Bind your fortunes to a new acquaintance on their ground.', {'standing':'-'}, 'The unhappy Mouse was soon suffocated by the water', True),
('new:doubtful_ally', 102, 'Swear friendship with an impressive ally from another field.', {'relationships.ally':'-'}, 'The Lion abused him as a traitor.', True),
('new:doubtful_ally', 182, 'Decline to join the weak side against the powerful.', {'relationships.weak_side':'-'}, M, False),
('new:doubtful_ally', 184, 'Refuse a loan guaranteed by someone known for taking and running.', {'relationships.borrower':'-'}, 'How then shall I be able to find you, when the day of payment comes?', False),
('new:doubtful_ally', 256, 'Argue both sides of a question in front of a new ally.', {'relationships.ally':'-'}, 'I can no longer consider you as a friend', True),
('new:doubtful_ally', 259, 'Take from a weaker neighbour who trusted you.', {'standing':'-'}, 'the eaglets, as yet unfledged and helpless, were roasted in their nest and dropped down dead at the bottom of the tree.', False),
('new:doubtful_ally', 277, 'Ask for a surety before allying with someone who can walk away.', {'relationships.ally':'-'}, M, False),
('new:doubtful_ally', 284, 'Doubt a large promise from someone who refuses a small favour now.', {'relationships.partner':'-'}, 'I can’t think that you, who refuse me a little matter now, will by and by confer on me a greater benefit.', False),
('new:doubtful_ally', 299, 'Accept a partner on the strength of their boasts.', {'relationships.partner':'-'}, 'brought back the shabbiest possible mouse, stinking from the length of time it had lain about the fields.', True),
('own_words_back', 120, 'Hold your companion to their own words.', {'relationships.companion':'-'}, M, False),
('own_words_back', 67, 'Rob the client while doing the job, then sue for your fee.', {'money':'-'}, 'I on the contrary affirm that I am still blind;', False),
('newcomer', 254, 'Fall in first with the idlest of the group.', {'standing':'-'}, 'the man put a halter on him and led him back to his owner.', True),
# ---- the lion's share, and predators let in -------------------------------------------------------
('new:lions_share', 3, 'Do the risky job first and ask for the fee after.', {'money':'-'}, M, True),
('new:lions_share', 101, 'Go into a venture with a far stronger partner.', {'money':'-'}, 'unless you willingly resign it to me, and set off as fast as you can.', True),
('new:lions_share', 158, 'Divide the spoils fairly with the powerful partner.', {'standing':'-'}, 'The Lion, bursting out into a great rage, devoured the Ass.', True),
('new:lions_share', 158, 'Leave yourself the smallest share.', {'money':'-', 'standing':'+'}, 'Who has taught you, my very excellent fellow, the art of division? You are perfect to a fraction.', True),
('new:lions_share', 274, 'Go it alone because the stronger partner takes most.', {'standing':'-'}, 'he himself fell prey to the huntsmen and hounds.', True),
('new:predator_inside', 83, 'Train someone to take from others for you.', {'relationships.pupil':'-'}, 'Since you have taught me to steal, you must keep a sharp lookout, or you will lose some of your own flock.', False),
('new:predator_inside', 129, 'Shut someone far more dangerous in with you.', {'money':'-'}, 'he flew upon the sheep and killed them, and then attacked the oxen.', True),
('new:predator_inside', 164, 'Warn the boss before the predator is let in.', {'relationships.boss':'+'}, 'Master, how can you expect the sheep to be safe if you admit a wolf into the fold?', False),
('new:predator_inside', 210, 'Leave someone long harmless in charge.', {'money':'-'}, 'The Wolf, now that he had the opportunity, fell upon the sheep, and destroyed the greater part of the flock.', True),
('new:predator_inside', 233, 'Refuse to admit a newcomer with a predator\'s instincts.', {'relationships.newcomer':'-'}, M, False),
('new:predator_inside', 281, 'Nurture newcomers likely to turn on everyone.', {'relationships.newcomers':'-'}, 'will inflict injury on all, beginning with yourself?', False),
('mercy_or_law', 86, 'Praise a small ill-gotten gain from someone in your charge.', {'relationships.charge':'-'}, 'he quickly seized her ear with his teeth and bit it off.', True),
# ---- heedless, warnings, records ------------------------------------------------------------------
('new:heedless', 13, 'Grab the next easy win without looking.', {'money':'-'}, 'If you had but touched me, my friend, you would have lost me, and all your locusts too!', False),
('new:heedless', 30, 'Go in because someone praises it, without checking the way out.', {'standing':'-'}, 'nor have exposed yourself to dangers from which you had no means of escape.', True),
('new:heedless', 32, 'Rush at what you need the moment it appears.', {'standing':'-'}, 'Having broken her wings by the blow, she fell to the ground, and was caught by one of the bystanders.', True),
('new:heedless', 62, 'Keep your eyes on lofty plans and ignore what is at your feet.', {'standing':'-'}, 'he fell accidentally into a deep well.', True),
('new:heedless', 107, 'Guard only against the danger you expect.', {'standing':'-'}, 'Some boatmen sailing by saw her, and taking a successful aim, mortally wounded her.', True),
('new:heedless', 140, 'Ask how you would get out before you go in.', {'standing':'+'}, M, False),
('new:heedless', 150, 'Doze on the edge of danger, and blame luck if you fall.', {'standing':'-'}, M, False),
('new:heedless', 216, 'Stay heavy and slow while danger comes for everyone.', {'standing':'-'}, 'the Geese, being slower of flight and heavier in their bodies, were captured.', True),
('new:heedless', 222, 'Stalk your target without watching your own feet.', {'standing':'-'}, 'he unknowingly trod upon a Viper asleep just before his feet.', True),
('new:heedless', 289, 'Swallow the new thing whole because it looks like the familiar.', {'standing':'-'}, 'Soon afterwards suffering great pain in his stomach', True),
('new:heedless', 308, 'Seize the easy prize lying unguarded.', {'standing':'-'}, 'The Serpent, turning about, bit the Crow with a mortal wound.', True),
('sign', 247, 'Sell your last safeguard on an early sign of good times.', {'money':'-'}, 'winter set in again with renewed frost and cold.', True),
('warning', 35, 'Ignore threats that have never been carried out.', {'standing':'-'}, 'The Farmer, on seeing this, charged his sling with stones, and killed a great number.', True),
('warning', 112, 'Prepare while things are quiet.', {'money':'-'}, M, False),
('warning', 214, 'Stay put while they only ask others, and move when they will act themselves.', {'standing':'+'}, 'It is time now to be off, my little ones, for the man is in earnest this time', False),
('warning', 241, 'Put off building your security once times are easy.', {'money':'+'}, 'it would be neither an easy nor a necessary work to make himself such a house as would accommodate him.', False),
('warning', 249, 'Warn early, though they call you mad.', {'standing':'+'}, 'they wondered at her knowledge and deemed her to be the wisest of birds.', True),
('go_and_look', 106, 'Agree the plan, then wait for someone else to carry it out.', {'standing':'-'}, 'there was no one found to do it.', False),
('new:bad_record', 54, 'Raise a real alarm after false ones.', {'money':'-'}, 'The Wolf, having no cause of fear, at his leisure lacerated or destroyed the whole flock.', True),
('new:bad_record', 104, 'Look for help after cheating everyone who could give it.', {'relationships.allies':'-'}, 'which of the gods do you think will pity you?', True),
('new:bad_record', 235, 'Make a show of a favour you could never have used yourself.', {'relationships.neighbour':'-'}, M, False),
('new:bad_record', 243, 'Make a new vow to a new patron after breaking the last.', {'relationships.patron':'-'}, 'how can I believe thee, who hast disowned and wronged thy former patron?', True),
('waiting', 267, 'Keep waiting for an out-of-season promise to come good.', {'money':'-'}, 'you are indulging a hope strong enough to cheat you, but which will never reward you with enjoyment.', False),
('waiting', 280, 'Wait on an angry threat that seems to promise you a windfall.', {'money':'-'}, 'The Wolf, hearing these words, went home, gasping with cold and hunger.', False),
# ---- criticism, the strong and the weak, revenge -------------------------------------------------
('new:pot_and_kettle', 38, 'Urge everyone to give up what you have lost.', {'standing':'-'}, 'If you had not yourself lost your tail, my friend, you would not thus counsel us.', True),
('new:pot_and_kettle', 84, 'Correct a junior for a fault you share.', {'standing':'-'}, 'The Mother tried in vain, and submitted without remonstrance to the reproof of her child.', True),
('new:pot_and_kettle', 111, 'Tell those who blame you to stop joining you.', {'relationships.critics':'-'}, 'Pray cease to flow into me, and then you will not be made briny.', False),
('new:pot_and_kettle', 116, 'Recommend your own trade as the answer to a crisis.', {'standing':'-'}, M, False),
('new:pot_and_kettle', 153, 'Condemn those above for what you do to those below.', {'standing':'-'}, 'Mercury presented himself, and striking the Philosopher with his wand', True),
('new:pot_and_kettle', 257, 'Find fault with everyone\'s work out of envy.', {'standing':'-'}, 'drove him from his office of judge, and expelled him from the mansions of Olympus.', True),
('wrongly_judged', 9, 'Point out that you have long been ready and are waiting on them.', {'relationships.boss':'-'}, M, False),
('called_to_account', 240, 'Turn the boss\'s anger back on the one accusing you.', {'standing':'+', 'relationships.accuser':'-'}, 'The Wolf was at once taken and flayed;', True),
('new:needling_the_strong', 49, 'Make a great deal of a small liberty.', {'standing':'-'}, 'A fine Lion you are, to be frightened of a Mouse.', False),
('new:needling_the_strong', 66, 'Taunt someone powerful while you are out of their reach.', {'standing':'+'}, M, False),
('new:needling_the_strong', 145, 'Sting the powerful again from your safe place.', {'standing':'+'}, 'The great do not always prevail.', True),
('new:needling_the_strong', 159, 'Attack someone bigger while they shelter from a worse threat.', {'relationships.bigger':'-'}, 'Let that monster go away and I will soon let you know what is the respective strength of a Goat and a Bull.', False),
('new:needling_the_strong', 252, 'Mock a colleague who has just been brought down.', {'standing':'-'}, 'While the Sparrow was thus speaking, a hawk suddenly seized him and killed him.', False),
('new:needling_the_strong', 285, 'Bully the weak and flatter the strong.', {'standing':'+'}, 'I know whom I may bully and whom I must flatter; and I thus prolong my life to a good old age.', False),
('new:needling_the_strong', 298, 'Pelt someone dangerous while they are down.', {'standing':'-'}, 'killing the Shepherds who had attacked him, raged with angry fury.', True),
('new:needling_the_strong', 298, 'Feed someone dangerous while they are down.', {'relationships.powerful':'+'}, 'I return as an enemy only to those who injured me.', True),
('new:needling_the_strong', 305, 'Strike at the small tormentor, whatever it costs you.', {'standing':'-'}, 'gave himself a heavy slap.', True),
('revenge', 149, 'Take a cruel revenge on the one who robbed you.', {'money':'-'}, 'the Farmer reaped nothing that year and returned home grieving sorely.', True),
('revenge', 166, 'Take your tormentor down with you.', {'standing':'-'}, 'At least my enemy and I shall perish together.', True),
('revenge', 244, 'Humiliate a guest with hospitality they cannot use.', {'relationships.guest':'-'}, 'The Fox, unable even to taste it, met with a fitting requital, after the fashion of her own hospitality.', True),
('revenge', 303, 'Pursue the powerful one who wronged you, even into their protector\'s presence.', {'relationships.powerful':'-'}, M, False),
('owning_up', 48, 'Offer peace to someone you hurt, who hurt you worse.', {'relationships.wronged':'-'}, 'There can henceforth be no peace between us;', True),
# ---- hard lots, used up, pleasing everyone, making them move --------------------------------------
('new:hard_lot', 61, 'Grasp the prickly problem boldly.', {'standing':'+'}, 'grasp it boldly, and it will be soft as silk to your hand, and not in the least hurt you.', False),
('new:hard_lot', 72, 'Keep bemoaning the post you have lost.', {'standing':'-'}, 'Forbear', False),
('new:hard_lot', 191, 'Try something new, a little at a time.', {'money':'+'}, 'until he brought the water within his reach and thus saved his life.', True),
('new:hard_lot', 195, 'Walk away saying you never wanted it.', {'standing':'+'}, 'The Grapes are sour, and not ripe as I thought.', False),
('new:hard_lot', 301, 'Rage against the confinement meant to protect you.', {'standing':'-'}, 'A violent fever suddenly set in, from which he died not many days later.', True),
('new:used_up', 74, 'Work a helper into the ground once the crisis has passed.', {'relationships.helper':'-'}, 'You must now go to the war on foot', True),
('new:used_up', 118, 'Leave before the one who sacrificed others turns to you.', {'relationships.boss':'-'}, M, False),
('new:used_up', 119, 'Ask to be praised for what you were, not blamed for what you are.', {'relationships.boss':'+'}, 'I rather deserve to be praised for what I have been, than to be blamed for what I am.', False),
('new:used_up', 161, 'Complain that you are always given the hardest jobs.', {'relationships.boss':'-'}, 'You have only to thank yourselves for the misfortunes to which you are exposed', False),
('new:please_everyone', 59, 'Let each party remake you to their taste.', {'relationships.parties':'-'}, M, True),
('new:please_everyone', 143, 'Change how you work at every critic\'s word.', {'money':'-', 'standing':'-'}, 'by endeavoring to please everybody he had pleased nobody, and lost his Ass in the bargain.', True),
('new:make_them_move', 14, 'Stop coaxing and use the tool you have.', {'money':'+'}, 'casting his net into the sea, made an excellent haul of fish.', True),
('new:make_them_move', 69, 'Tell your successors there is treasure hidden in the work.', {'money':'+'}, 'the vines repaid their labor by an extraordinary and superabundant crop.', True),
('new:make_them_move', 242, 'Use warmth, not force.', {'standing':'+'}, 'The Traveler no sooner felt his genial rays than he took off one garment after another', True),
('new:make_them_move', 242, 'Push harder.', {'standing':'-'}, 'the keener his blasts, the closer the Traveler wrapped his cloak around him', True),
]
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Render groups.json + records.json + options_by_entry.json as one self-contained page: REFERENCE.html."""
import html, json, pathlib
here = pathlib.Path(__file__).parent
d = json.loads((here / 'groups.json').read_text())
recs = {r['chunk']: r for r in json.loads((here / 'records.json').read_text())}
stories = json.loads((here.parent / 'stories.json').read_text())
obe = json.loads((here / 'options_by_entry.json').read_text())
new = obe.pop('_new_entries')
e = html.escape
SRC = {'archetype': 'archetype', 'filled': 'filled gap'}
def src(o):
if o['source'].startswith('scene:'):
n = o['source'].split(':')[1]
return f'from fable {n}' + ('' if o['in_group'] else ' <span class="warn">(outside this group)</span>')
return SRC[o['source']]
def moves(m): return ' '.join(f'<code>{e(k)} {e(v)}</code>' for k, v in m.items())
def scene(c):
r = recs[c]
moral = f'<br><i>Moral:</i> {e(r["moral"])}' if r['moral'] else ''
return (f'<details><summary><b>{c}</b> · <i>{e(stories[r["story"]]["title"])}</i> — {e(r["predicament"])}</summary>'
f'<p class="sum">{e(r["summary"])}{moral}</p></details>')
def section(g):
tag = ' <span class="warn">small group, below the 13-fable rule</span>' if g['small'] else ''
return f'''<section><h2>{g["id"]} · {e(g["name"])}{tag}</h2>
<p class="pred">{e(g["predicament"])}</p>
<p class="meta">{e(g["sentence"])} · {len(g["members"])} fables · stage <b>{g["stage"]}</b></p>
<h3>Options</h3><ol>{"".join(f"<li>{e(o['text'])} <span class='src'>{src(o)}</span><br>{moves(o['moves'])}</li>" for o in g["options"])}</ol>
<h3>Fables</h3>{"".join(scene(m["chunk"]) for m in g["members"])}</section>'''
big = ''.join(section(g) for g in d['groups'] if not g['small'])
small = ''.join(section(g) for g in d['groups'] if g['small'])
un = ''.join(scene(c) for c in d['unassigned'])
def entry(k, v):
name = new[k]['name'] + ' <span class="src">(new)</span>' if k in new else e(k)
rows = ''.join(f'<li>{e(o["text"])} {moves(o["moves"])} <span class="src">fable {o["source"].split(":")[1]} · '
f'{"<b>stated</b>" if o["stated"] else "inferred"}</span><br><span class="sum">“{e(o["evidence"])}”</span></li>'
for o in v)
return f'<details><summary><b>{name}</b> — {len(v)} options, {sum(o["stated"] for o in v)} stated</summary><ul>{rows}</ul></details>'
opts = [o for v in obe.values() for o in v]
meas = ''.join(entry(k, v) for k, v in obe.items())
page = f'''<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Aesop Reference Groups</title>
<style>
:root{{--bg:#fbfaf7;--fg:#1d1d1b;--mute:#6b6760;--line:#e2ded6;--acc:#2f5d8a;--warn:#a4481c}}
@media (prefers-color-scheme:dark){{:root:not([data-theme="light"]){{--bg:#191917;--fg:#e8e6e1;--mute:#a09c94;--line:#34332f;--acc:#8ab4dc;--warn:#e6915f}}}}
:root[data-theme="dark"]{{--bg:#191917;--fg:#e8e6e1;--mute:#a09c94;--line:#34332f;--acc:#8ab4dc;--warn:#e6915f}}
body{{background:var(--bg);color:var(--fg);font:16px/1.5 Georgia,serif;max-width:52rem;margin:0 auto;padding:1rem 16px 4rem}}
h1{{font-size:1.5rem}} h2{{font-size:1.15rem;margin:0 0 .3rem}} h3{{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mute);margin:.9rem 0 .3rem}}
section{{border-top:1px solid var(--line);padding:1.2rem 0}} .pred{{font-size:1.05rem;margin:.2rem 0}}
.meta,.src,.sum{{color:var(--mute);font-size:.88rem}} .warn{{color:var(--warn);font-size:.85rem}}
code{{font:12px ui-monospace,monospace;border:1px solid var(--line);border-radius:3px;padding:0 .3em;margin-right:.2em;overflow-wrap:anywhere}}
li{{margin:.35rem 0}} details{{margin:.2rem 0}} summary{{cursor:pointer}} .sum{{margin:.2rem 0 .5rem 1.2rem}}
</style></head><body>
<h1>Aesop's Fables: reference groups</h1>
<p class="meta"><i>Three Hundred Aesop's Fables</i>, tr. George Fyler Townsend, 1867 (Project Gutenberg). 313 fables, one scene
per fable. Summaries, predicaments, groups, options and the options measurement written by Claude reading every fable:
no inference of any kind. A comparison baseline, not a pack. Option moves give a direction only, never a number.</p>
{big}
<section><h2>Small groups (5-12 fables)</h2><p class="meta">Situations that recur, but below the 13-fable rule this corpus's size sets.</p></section>
{small}
<section><h2>Unassigned ({len(d["unassigned"])})</h2><p class="meta">Fables that fit no situation recurring in five or more fables.</p>{un}</section>
<section><h2>Fables as an option source</h2>
<p class="meta">{len(opts)} options for {len(obe)} catalogue situations, {sum(o["stated"] for o in opts)} of them
<b>stated</b>: the fable's own outcome or moral shows where every move goes. The rest are inferred.</p>{meas}</section>
</body></html>'''
(here / 'REFERENCE.html').write_text(page, encoding='utf-8')
print('wrote', here / 'REFERENCE.html', len(page), 'bytes')
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
"""Split the Project Gutenberg text of *Three Hundred Aesop's Fables*, translated by George Fyler Townsend (1867,
eBook #21), one story per fable and ONE SCENE PER FABLE. This departs from the usual scene rule on purpose:
most fables are under 80 words, so the rule's 80-word tail would discard them, and a fable split in two would
lose its turn. Titles come from the contents (after its AESOP'S FABLES line); each must be found, in order, as
a line of its own in the body. The preface and the life of Aesop are dropped. The printed drop capital was lost
in transcription, so many fables open with the first word's initial letter missing (e.g. "WOLF" for "A WOLF")."""
import json, pathlib, re, sys
sys.path.insert(0, '..')
from gutenberg_chunks import gutenberg_body, paragraphs
VOLUME = "Three Hundred Aesop's Fables (tr. George Fyler Townsend, 1867)"
lines = gutenberg_body('corpus/three-hundred-aesops-fables.txt')
c0 = next(i for i, l in enumerate(lines) if l.strip() == 'CONTENTS')
f0 = next(i for i, l in enumerate(lines) if i > c0 and l.strip().upper().replace('Æ', 'AE') in ("AESOP’S FABLES", "AESOP'S FABLES"))
titles = []
for l in lines[f0 + 1:]:
if l.strip(): titles.append(l.strip())
elif titles and not any(x.strip() for x in lines[lines.index(l) + 1:lines.index(l) + 3]): break
titles = [t for t in titles if t not in ('FOOTNOTES', 'INDEX')] # back matter listed in the contents
body0 = next(i for i, l in enumerate(lines) if i > f0 + len(titles) and l.strip() == titles[0])
heads, at = [], body0
for t in titles:
at = next(i for i in range(at, len(lines)) if lines[i].strip() == t and not lines[i].startswith(' '))
heads.append(at); at += 1
end = next(i for i in range(heads[-1] + 1, len(lines)) if lines[i].strip() in ('FOOTNOTES', 'INDEX'))
stories, chunks = [], []
for n, (t, a) in enumerate(zip(titles, heads)):
text = ' '.join(paragraphs('\n'.join(lines[a + 1:(heads[n + 1] if n + 1 < len(heads) else end)])))
w = len(text.split())
stories.append({'volume': VOLUME, 'title': t, 'words': w, 'text': text})
chunks.append({'story': n, 'title': t, 'volume': VOLUME, 'text': text, 'words': w})
pathlib.Path('stories.json').write_text(json.dumps(stories, indent=1, ensure_ascii=False), encoding='utf-8')
pathlib.Path('chunks.json').write_text(json.dumps(chunks, ensure_ascii=False), encoding='utf-8')
ws = sorted(c['words'] for c in chunks)
print(f'fables {len(stories)} words/fable min {ws[0]} median {ws[len(ws)//2]} max {ws[-1]} total {sum(ws):,}')
print('shortest:', min(stories, key=lambda s: s['words'])['title'], '| longest:', max(stories, key=lambda s: s['words'])['title'])
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
test: 41 candidates, 5 labelled 2
FROZEN RULE S1 >= 0.6119 AND S6 <= 0.1603
accepted 5 P2 0.40 P1+ 1.00 R2 0.40
baseline accept all candidates accepted 41 P2 0.12 P1+ 0.51 R2 1.00
baseline B0: tightness filter accepted 18 P2 0.17 P1+ 0.44 R2 0.60
accepted:
T05 label 1 schemes, cons and theft over money
T07 label 1 courtship
T25 label 1 trying to impress or win someone's favour
T35 label 2 coercion over money: threats, bribes, demands for payment
T39 label 2 a spouse schemes against or confronts a spouse over infidelity
rejected label-2:
T16 a newcomer seeks acceptance in a social circle
T17 a proposal or engagement, accepted or refused
T30 a run-in with the police
+77
View File
@@ -0,0 +1,77 @@
"""Check a saved review and turn it into situations.json.
python3 apply_review.py review/k60-s24 # finds the saved review itself
python3 apply_review.py review/k60-s24 ~/Downloads/review-k60-s24.json
Without a file named, it uses the newest review-<run>*.json in the run directory, and
otherwise the newest one in ~/Downloads, where a browser saves it. It prints every problem
at once and writes nothing until the review passes; then it writes situations.json into the
run directory.
"""
import datetime, json, os, pathlib, sys
import review_format as rf
def downloads_dir():
return pathlib.Path(os.environ.get('STP_DOWNLOADS', pathlib.Path.home() / 'Downloads'))
def find_review(run_dir, run_id):
for folder in (run_dir, downloads_dir()):
if folder.is_dir():
found = sorted(folder.glob(f'review-{run_id}*.json'), key=lambda p: p.stat().st_mtime, reverse=True)
if found:
return found[0]
return None
def main(argv):
if len(argv) not in (2, 3):
print(__doc__.strip())
return 2
run_dir = pathlib.Path(argv[1])
cand_path = run_dir / 'candidates.json'
if not cand_path.exists():
print(f'{run_dir} has no candidates.json. Check the folder name, or run review.py first.')
return 1
candidates = json.loads(cand_path.read_text(encoding='utf-8'))
run_id = candidates['run']['id']
review_path = pathlib.Path(argv[2]).expanduser() if len(argv) == 3 else find_review(run_dir, run_id)
if review_path is None or not review_path.exists():
print(f'No saved review found for run {run_id}.')
print(f'Open {run_dir / "REVIEW.html"} in a browser, and at the end press "Save review file".')
print(f'It saves review-{run_id}.json into Downloads, where this looks for it.')
return 1
print(f'reading {review_path}')
try:
form = json.loads(review_path.read_text(encoding='utf-8'))
except json.JSONDecodeError:
print('That file is damaged and cannot be read. Save the review again from REVIEW.html; '
'your progress is still in the browser.')
return 1
errors, warnings, result = rf.validate(candidates, form)
for w in warnings:
print(f'note: {w}')
if errors:
print(f'\nThe review is not finished. {len(errors)} thing(s) to sort out in REVIEW.html, '
f'then save the review file again:')
for e in errors:
print(f' - {e}')
return 1
result['applied'] = datetime.datetime.now().astimezone().isoformat(timespec='seconds')
result['review_file'] = str(review_path)
out = run_dir / 'situations.json'
out.write_text(json.dumps(result, indent=1, ensure_ascii=False), encoding='utf-8')
c = result['counts']
print(f'\n{out}: {c["situations"]} situations ({c["kept"]} kept, {c["same"]} merged in, '
f'{c["dropped"]} dropped, {c["left_out"]} extras left out).')
for s in result['situations']:
print(f' {s["id"]} {s["situation"]} ({s["size"]} scenes, {s["stories"]} stories)')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
+106
View File
@@ -0,0 +1,106 @@
# Bierce reference set: *Tales of Soldiers and Civilians* (1891)
A hand-made comparison baseline for the story-to-pack pipeline, a sibling of the Jacobs set in
`../jacobs/` and the Wharton set in `../wharton/`. Claude read every scene and wrote each summary,
predicament, group and option. **No local inference was used.** Compare the local-model runs against it.
Do not treat it as ground truth: it is one careful reading.
| File | What it is |
| --- | --- |
| `corpus/tales-of-soldiers-and-civilians.txt` | Project Gutenberg #13334 ("Collected Works of Ambrose Bierce, Vol. II", which holds this book and nothing else) |
| `split_gutenberg.py` | → `stories.json` (26 stories: 15 "Soldiers", 11 "Civilians") and `chunks.json` (299 scenes, same 320-word paragraph rule as `../chunk.py`) |
| `dump.py` | `python3 dump.py A B` prints scenes A..B for reading |
| `claude/batches/sNN.json` | per story: `{chunk, story, summary, predicament}`, the same schema as `../jacobs/claude/batches/` |
| `claude/groups_src.py` | the groups, options and per-story protagonist standing |
| `claude/build.py` | checks everything, then writes `claude/records.json` and `claude/groups.json` (unchanged from Jacobs: it resolves paths relative to itself) |
| `claude/page.py` | → `claude/REFERENCE.html`, the readable page |
Rebuild: `python3 split_gutenberg.py && cd claude && python3 build.py && python3 page.py`.
## What is in it
- **299 predicaments**: one sentence each (median 20 words, min 15, max 24), written to the
`redescribe.py` rules: no names, jobs, ranks, places or story-only objects. The `topic_words`
leak check flags 20 of 299. 18 are role or kin words (partner, newcomer, visitor, lover, father,
guest), kept on purpose because the design's cast is roles. The other 2 are "judge" used as a
verb. War roles are kept as roles, not ranks: *enemy*, *comrades*, *superior*, *subordinate*,
*prisoner*, *captor*.
- **31 groups**, each a situation that recurs in **three or more stories** (18 span 3, 10 span 4,
2 span 5, 1 spans 6). No story supplies more than half of any group's members. Each scene sits in at
most one group: **201 are assigned and 98 fit nothing that recurs**. All 26 stories feed at
least one group.
- **Options**: 3 to 5 per group, 128 in all (25 groups have 4, 5 have 5, 1 has 3). 96 come from what
someone in a scene did or refused to do (`scene:N`), 15 are archetypes and 17 fill gaps. **22
scene options cite a scene outside their own group.** That is looser than DESIGN.md's "drawn from
other scenes in the cluster", and `groups.json` marks each one `in_group: false`. As in Wharton,
most of these are the scene just before or after a member, where the character acts.
- **Moves**: spine paths with a direction only (`+`, `-`, `risk`, `set`). There are no magnitudes,
as the design requires. `standing` is moved 65 times, the two skills 66 times and `money` only 9.
Relationship roles are generic (`authority`, `comrades`, `loved_one`, `family`, `rival`, `captor`,
`doubters` and others), and flags record what the player did (`slept_on_watch`,
`duty_over_love`, `struck_blind`, `took_dare`, `guilty_secret`).
- **Skills** proposed for this corpus: `nerve`, reused from Jacobs, for holding steady under fear, and
`wits`, for reading signs and thinking coolly in a tight spot. `wits` fits Bierce's scouts,
trapped men and sceptics better than Jacobs' `cunning` would.
- **Stage** comes from protagonist standing, judged **per story, not per scene**, in `STANDING` in
`groups_src.py`. Enlisted men, a captive, a child, a pauper and two frontier loners rank low.
Officers and professionals rank middle. A planter, a governor, a camp's leading citizen, a rich
idler and an heiress rank high. The groups come out 19 middle, 9 low and 3 high. The skew is the
book's own: most soldier stories are told from an officer's side.
## Against the existing catalogue
The 36-entry catalogue (O. Henry, Wharton, Jacobs) is mostly courtship, secrets and schemes.
**Nine groups overlap an existing entry closely:** G04 private score (`power_over_you`),
G13 alone with the dead (`dread`), G19 truth nobody would believe (`cant_prove_it`), G21 judged
before heard (`wrongly_judged`), G22 tempting chance (`sure_thing` / `strings`), G26 devoted to
one who wronged you (`sacrifice`), G27 someone thought gone turns up (`past_comes_back`), G31 new
to a hardened company (`newcomer`) and G18 warning easy to laugh off (between `dread` and `sign`).
**Three overlap in part:** G03 told to obey without question (`pressed_to_wrong`, plus
`own_words_back` in scene 138), G05 stranger not what they seem (`sure_thing`) and G20 careless
words cost a life (`owning_up`).
**Nineteen are new situation types**:
- **Duty and orders:** G02 duty demands you harm someone you love; G06 your heart is on the other side.
- **Watch and danger:** G01 on watch, and your guard slips; G07 the only way to know is to go and look; G17 the danger you can only wait for; G16 cornered, looking for a way out; G15 condemned and waiting to die.
- **Courage and fear:** G09 refusing the safe way out lest it look like fear; G10 your courage called into question; G11 finding out you are afraid; G12 a gaze you cannot escape.
- **Harm to others:** G14 the one you struck in the dark (mistaken-identity violence); G08 one bold act sweeps everyone into slaughter.
- **Help and suffering:** G23 turned away when you need help most; G24 watching others suffer from safety; G25 trying to save someone who will not be saved.
- **Loss and isolation:** G28 coming home too late; G29 alone with an old grief; G30 lost in the dark, far from help.
## How this corpus differs from Jacobs and Wharton
It is the largest reference set so far, 26 stories against Jacobs' 12 and Wharton's 10. That is
the first corpus inside DESIGN.md's 20-30 range. So the bar was raised to three stories per group,
and 31 groups still cleared it.
**The register is war and horror, and the situations are mostly inner ones.** Where Jacobs and
Wharton recur on money, marriage and secrets, Bierce recurs on fear, duty, courage and dying. That
shows in the moves. `money` hardly appears, and `nerve` carries much of the weight. A pack built
from this corpus will need its numeric search to make `nerve` and `standing` do the work that
`money` does in the other packs.
**Groups straddle the two halves of the book**, and a tool that groups by setting or mood should
split them. G14 "the one you struck in the dark" holds a soldier who shoots his twin brother on
picket and a lawyer who shoots a woman he takes for a panther. G12 "a gaze you cannot escape"
holds a trapped scout staring into his own rifle, a guest facing a snake, a mother facing a
panther and a girl under a mesmerist. G10 "your courage called into question" holds a
battlefield suicide and a ghost-story dare.
**Twist endings concentrate meaning in the last scene.** Many stories turn on a final line: the
rider was his father, the snake was stuffed, the applicant was the founder, the child was a deaf
mute. Those closing scenes often belong to a different situation from the story's body, or to
none. That is one reason 98 scenes stay unassigned. Most of the rest are scene-setting narration
(the Chickamauga march, Owl Creek's dying dream, the Coulter's Notch gun duel) or one-story
material with no second instance, such as the mercy killing in *The Coup de Grâce*.
## Known defects
- Scene 166 ends with the stray part heading `CIVILIANS`. The in-story subtitles and section headings of
*A Son of the Gods*, *An Affair of Outposts*, *One Kind of Officer*, *George Thurston*, *The
Suitable Surroundings* and *The Eyes of the Panther* (for example "HOW TO SAY WHAT IS WORTH
HEARING") stay inline in scene text. They are
not removed, so that `chunks.json` stays the corpus as split.
- Scene 227 is only the mock-archaic epigraph of *The Man and the Snake*. It was summarised anyway
and is in no group.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,62 @@
[
{
"chunk": 0,
"story": 0,
"summary": "A Union soldier lies asleep at his post in a clump of laurel on a cliff-top above a valley, a crime for which the lawful penalty is death.",
"predicament": "Someone trusted to keep watch has fallen asleep at the post, where being found out would cost them their life."
},
{
"chunk": 1,
"story": 0,
"summary": "Five regiments lie hidden in the valley below, meaning to climb past the sleeping sentinel at nightfall and surprise the enemy camp; if they are seen, they are trapped.",
"predicament": "A whole company's plan depends on staying unseen, and one careless lookout could give every one of them away."
},
{
"chunk": 2,
"story": 0,
"summary": "The sentinel, only son of a wealthy Virginian family, had told his father he was joining the Union; the father called him a traitor, told him to do his duty, and asked him not to disturb his dying mother.",
"predicament": "A son tells his father he is going over to the other side, and is told coldly to do his duty."
},
{
"chunk": 3,
"story": 0,
"summary": "He left home, proved himself brave and was picked for this dangerous outpost, but fatigue beat him; now something wakes him and he grips his rifle.",
"predicament": "Someone who left home to prove their worth wakes suddenly at a dangerous post, having failed the trust placed in them."
},
{
"chunk": 4,
"story": 0,
"summary": "He sees on the cliff edge a Confederate horseman, motionless as a statue, looking down into the valley where the regiments hide.",
"predicament": "Someone on watch sees an enemy standing in plain view, looking straight down at the comrades they are meant to protect."
},
{
"chunk": 5,
"story": 0,
"summary": "He aims at the horseman's breast, but the rider turns and seems to look into his eyes; the young soldier shakes, turns faint and drops his face to the leaves.",
"predicament": "Someone who must kill a stranger to protect their comrades is overcome by horror at the very moment of doing it."
},
{
"chunk": 6,
"story": 0,
"summary": "Recovering, he tells himself the rider may have seen nothing and may ride away, then sees an escort watering horses in plain view in the meadow below; the secret is out.",
"predicament": "Someone hoping they will not have to kill sees proof that the other already knows the secret that endangers everyone."
},
{
"chunk": 7,
"story": 0,
"summary": "Recalling his father's parting words about duty, he aims at the horse instead and fires calmly; below, an officer looks up and sees a horseman riding down through the air.",
"predicament": "Someone forced to choose between duty and the life of a person they love remembers a parent's words, and fires."
},
{
"chunk": 8,
"story": 0,
"summary": "The officer watches horse and rider fall, collapses with terror, then searches for the body in the wrong place and returns to camp.",
"predicament": "Someone who has just seen something impossible is too shaken to make sense of it, and searches in the wrong place."
},
{
"chunk": 9,
"story": 0,
"summary": "The officer, knowing better than to tell an incredible truth, says nothing of it; the sentinel tells a sergeant he shot a horse off the rock, and that the rider was his father.",
"predicament": "Someone who has killed their own parent in the line of duty is ordered to report it, and answers without emotion."
}
]
@@ -0,0 +1,92 @@
[
{
"chunk": 10,
"story": 1,
"summary": "A man with bound hands and a noose round his neck stands on planks laid across a railroad bridge, attended by two soldiers and a sergeant, while a captain watches and sentinels close both ends.",
"predicament": "Someone bound and condemned stands at the edge of their own death while the people carrying it out go calmly about it."
},
{
"chunk": 11,
"story": 1,
"summary": "A fort commands the bridge and a company of infantry stands motionless in line to witness the hanging; nobody moves, as military etiquette requires in the presence of death.",
"predicament": "A condemned person faces a silent crowd drawn up to watch, and not one of them will move to help."
},
{
"chunk": 12,
"story": 1,
"summary": "The condemned man is a well-dressed civilian planter of about thirty-five with a kindly face, plainly no common criminal.",
"predicament": "A respectable person, plainly no common criminal, is about to be put to death for something done in a cause."
},
{
"chunk": 13,
"story": 1,
"summary": "The plank he stands on is now held only by the sergeant's weight; unblindfolded, he stares at the water and watches a piece of driftwood that seems to move very slowly.",
"predicament": "Someone whose life hangs on another's single step waits for the signal, and finds their mind drifting to trifles."
},
{
"chunk": 14,
"story": 1,
"summary": "Trying to think of his wife and children, he is tormented by the loud ticking of his watch; he imagines freeing his hands, diving and escaping home, and the captain nods to the sergeant.",
"predicament": "Someone about to die tries to fix their last thoughts on their family, and instead finds themselves planning a desperate escape."
},
{
"chunk": 15,
"story": 1,
"summary": "In a flashback, the planter, an ardent secessionist kept out of the army, longs for distinction; a grey-clad rider tells him that any civilian caught interfering with the railroad will be hanged.",
"predicament": "Someone who has longed to prove themselves in a cause is told of a dangerous chance to do it, and a harsh penalty."
},
{
"chunk": 16,
"story": 1,
"summary": "The rider tells him the bridge is lightly guarded and that dry driftwood lodged against it would burn; the planter is tempted, and the rider turns out to be an enemy scout.",
"predicament": "A friendly stranger hands someone eager to prove themselves an easy chance at glory, and it is really a trap."
},
{
"chunk": 17,
"story": 1,
"summary": "Falling through the bridge, he feels agony and swinging, then plunges into the water; he believes the rope has broken and rises toward the surface, determined not to be shot.",
"predicament": "Someone who should already be dead believes they have been given an impossible second chance, and grabs at it."
},
{
"chunk": 18,
"story": 1,
"summary": "His hands seem to free themselves and tear off the noose; in agony, he surfaces and draws a great shrieking breath.",
"predicament": "Someone whose body fights to live against their own will struggles to the surface in unbearable pain."
},
{
"chunk": 19,
"story": 1,
"summary": "His senses are unnaturally sharp; he sees every leaf and insect, and turning, sees his executioners on the bridge pointing and shouting at him.",
"predicament": "Someone escaping death sees the world with impossible clarity, and sees the people who meant to kill them pointing at them."
},
{
"chunk": 20,
"story": 1,
"summary": "The sentinels fire and miss; a lieutenant calmly orders a volley, and he dives deep as the bullets sink around him.",
"predicament": "Someone fleeing for their life hears the calm order that will send a volley after them, and has only seconds to hide."
},
{
"chunk": 21,
"story": 1,
"summary": "He swims downstream, reasoning coolly about the next volley, as the fort's cannon sends a shot plunging near him; he resolves to watch the gun's smoke.",
"predicament": "Someone hunted by a far stronger force keeps their head and tries to reason out the enemy's next move."
},
{
"chunk": 22,
"story": 1,
"summary": "A whirlpool flings him ashore out of sight; he weeps with joy, then grapeshot sends him running into a forest that seems endless and uncanny.",
"predicament": "Someone who seems to have escaped certain death is overwhelmed with joy, then finds the familiar world strangely changed."
},
{
"chunk": 23,
"story": 1,
"summary": "Exhausted and starving, he follows a strange straight road under unfamiliar stars, hearing whispers in an unknown tongue, his neck swollen and his feet no longer feeling the ground.",
"predicament": "Someone struggling home to their family finds the road and the stars wrong, and their own body failing them."
},
{
"chunk": 24,
"story": 1,
"summary": "He reaches his home and his wife comes smiling to meet him, but as he reaches for her there is a blow and blinding light; he was hanged, and his body swings beneath the bridge.",
"predicament": "Someone reaches home and the person they love at last, only for it all to prove a dying dream."
}
]
@@ -0,0 +1,62 @@
[
{
"chunk": 25,
"story": 2,
"summary": "A small child slips away from his poor home into a forest, delighted by freedom and the chance of adventure.",
"predicament": "A child who has slipped away from home unseen is thrilled by freedom, with no idea of the danger ahead."
},
{
"chunk": 26,
"story": 2,
"summary": "The six-year-old son of a planter who was once a soldier plays at war with a wooden sword, chasing imaginary enemies across a brook far from home.",
"predicament": "A child playing at the glory an elder taught them pushes further and further from safety, sure of their own victory."
},
{
"chunk": 27,
"story": 2,
"summary": "A rabbit sends him fleeing in terror; lost and torn by brambles, he cries himself to sleep between two rocks while distant thunder sounds and his mother searches frantically at home.",
"predicament": "A child who ran from a small fright is lost far from home, while a parent searches desperately and cannot find them."
},
{
"chunk": 28,
"story": 2,
"summary": "Waking at dusk, he goes the wrong way and sees strange shapes approaching, many of them, all moving toward the brook.",
"predicament": "A lost child alone at dusk sees strange shapes creeping toward them, too many to count, and cannot tell what they are."
},
{
"chunk": 29,
"story": 2,
"summary": "They are wounded men, hundreds of them, crawling toward the creek on hands and knees; some stop and die, others gesture as if praying.",
"predicament": "Someone comes upon a vast crowd of the dying, all crawling silently toward water, and does not understand what they see."
},
{
"chunk": 30,
"story": 2,
"summary": "The child thinks the bloodied men are clowns and rides one like a horse; the man, whose lower jaw is shot away, throws him off and shakes a fist, and the child hides behind a tree.",
"predicament": "An innocent treats other people's agony as a game, until one of the suffering turns on them in fury."
},
{
"chunk": 31,
"story": 2,
"summary": "A red light rises beyond the trees; the child puts himself at the head of the crawling wounded with his wooden sword and solemnly leads them toward it.",
"predicament": "An innocent who understands nothing of the disaster around them puts themselves at the head of its victims and leads them on."
},
{
"chunk": 32,
"story": 2,
"summary": "The ground is littered with the debris of a retreat; a battle had been fought all around the sleeping child hours before, and he heard none of it.",
"predicament": "Someone slept through a catastrophe that happened all around them, and now walks through its wreckage without knowing it."
},
{
"chunk": 33,
"story": 2,
"summary": "The stones in the creek are red with blood; the strongest of the wounded drink and drown, and the child waves his cap to encourage the rest toward the fire.",
"predicament": "Someone cheerfully urges on a group of the doomed, not seeing that those who reached what they wanted have died of it."
},
{
"chunk": 34,
"story": 2,
"summary": "He reaches a burning house and dances before it, then recognises it as his own home and finds his mother dead, killed by a shell; he is a deaf mute and can only make wordless cries.",
"predicament": "Someone returns to the home they wandered from and finds it destroyed and the person they love dead, and cannot even cry out."
}
]
@@ -0,0 +1,68 @@
[
{
"chunk": 35,
"story": 3,
"summary": "An advancing army halts at the edge of a wood before open country and a stone wall on a distant crest; after days of fighting the enemy has vanished, and nobody knows whether they wait behind the wall.",
"predicament": "A large group must cross open ground toward a place where an enemy may be waiting, and nobody knows whether they are."
},
{
"chunk": 36,
"story": 3,
"summary": "The narrator reflects on how strange the enemy's leavings seem, and reads from the tracks that the enemy withdrew in good order up the hill rather than fleeing.",
"predicament": "People who have fought an unseen enemy for days study the signs they left, trying to guess whether they are truly gone."
},
{
"chunk": 37,
"story": 3,
"summary": "The commander orders the skirmish line forward; a young officer in full dress on a white horse with a scarlet blanket gallops up, and the whole line laughs at his vanity.",
"predicament": "A showy young newcomer rides in front of hardened veterans, who laugh at him as a vain fool."
},
{
"chunk": 38,
"story": 3,
"summary": "The young officer asks the commander something the older man is reluctant to grant, then rides alone at a walk straight toward the wall; the skirmishers are halted, and the watching army is stirred to adoration and shame for having laughed.",
"predicament": "Someone the crowd mocked a moment ago volunteers to walk alone toward almost certain death so that the others need not."
},
{
"chunk": 39,
"story": 3,
"summary": "The whole army watches in silence; the horseman swerves toward a break in the wall, meaning to ride through and see what lies beyond.",
"predicament": "Someone takes a mortal risk alone while a whole crowd holds its breath, unable to do anything but watch."
},
{
"chunk": 40,
"story": 3,
"summary": "The narrator explains that skirmishers sent to find out whether the enemy holds the ridge would be slaughtered; the young officer is paying the price alone.",
"predicament": "The only way to learn whether danger waits ahead is to send someone into it, and one person offers to pay the whole price."
},
{
"chunk": 41,
"story": 3,
"summary": "He could surrender and live, but that would answer nothing; for his errand to succeed he must either return unharmed or be shot dead before his comrades' eyes.",
"predicament": "Someone on a lonely errand could save themselves by giving up, but that would make the whole sacrifice worthless."
},
{
"chunk": 42,
"story": 3,
"summary": "Having seen the enemy line, he gallops back and forth along the crest trying to force the hidden troops to reveal themselves by firing on him, while they hold their fire.",
"predicament": "Someone who has found out the truth cannot bring word back, so they try to force the enemy to reveal it themselves."
},
{
"chunk": 43,
"story": 3,
"summary": "He dashes at the wall, then turns and races back; the wall erupts in rifle fire and artillery, he survives the first volley to a great cheer, and shells begin falling among the watching troops.",
"predicament": "Someone who has drawn every enemy weapon onto themselves races for home while their comrades cheer and the danger spreads to them."
},
{
"chunk": 44,
"story": 3,
"summary": "His horse is killed; he stands and salutes with his sabre before he falls, and his comrades, choking with emotion, surge forward against orders into the enemy's fire.",
"predicament": "A crowd moved by one person's sacrifice rushes forward against orders, throwing away the very lives it was meant to save."
},
{
"chunk": 45,
"story": 3,
"summary": "The commander, unmoved, sounds the halt; the lines fall back carrying their wounded and the dead, and the narrator laments the many needless deaths and the hero's vain devotion.",
"predicament": "Someone in command must call back a crowd carried away by grief, knowing the sacrifice that inspired them was wasted."
}
]
@@ -0,0 +1,122 @@
[
{
"chunk": 46,
"story": 4,
"summary": "A fearless private detailed as a scout slips out past the earthworks, sent by his general to creep as close to the enemy lines as possible and learn all he can.",
"predicament": "Someone trusted for their nerve is sent alone into hostile ground to find out what the enemy is doing."
},
{
"chunk": 47,
"story": 4,
"summary": "He passes the picket line, where a man remarks that the enemy will soon have his rifle, and creeps forward through the thicket, steady and alert.",
"predicament": "Someone creeping alone toward an enemy's lines hears their own comrades already writing them off as dead."
},
{
"chunk": 48,
"story": 4,
"summary": "He sees an enemy rifle-pit, watches it, reads the signs correctly and stands up: the enemy has gone.",
"predicament": "Someone creeping toward danger must judge from small signs whether the enemy is still there, and stake their life on the answer."
},
{
"chunk": 49,
"story": 4,
"summary": "To be certain, he pushes on to a derelict plantation and hides in a tumbledown shed on posts, from which he sees the enemy's rear-guard climbing a mountain road.",
"predicament": "Someone who already knows enough to report pushes further on to be certain, into a shaky and exposed hiding place."
},
{
"chunk": 50,
"story": 4,
"summary": "His duty is to hurry back and report, but the retreating column tempts him, and he cocks and sets his rifle to fire into it.",
"predicament": "Someone whose duty is to hurry back with news is tempted to stay and strike a blow that changes nothing."
},
{
"chunk": 51,
"story": 4,
"summary": "The narrator traces how fate brought a deserter from Europe to command an enemy battery two miles away, where, idling, he fires a shot at what he takes for enemy officers, and it flies high.",
"predicament": "A careless act by a stranger far away, done only to pass the time, is about to decide someone's fate."
},
{
"chunk": 52,
"story": 4,
"summary": "The stray shell smashes a post and brings the shed down on him; half-conscious, he imagines himself buried with his widow kneeling on his grave.",
"predicament": "Someone knocked senseless in an instant wakes believing they are already dead and buried, unable to speak to those they love."
},
{
"chunk": 53,
"story": 4,
"summary": "Coming round, he hears a voice saying he is caught like a rat in a trap, and calmly opens his eyes to reconnoitre and plan his defence.",
"predicament": "Someone who realises they are trapped gathers themselves to size up their situation calmly, as if it were an enemy."
},
{
"chunk": 54,
"story": 4,
"summary": "He finds himself pinned by beams across his chest, legs and head, with only his right arm partly free; he is unhurt.",
"predicament": "Someone pinned and helpless, unhurt but unable to move, takes stock of the little freedom left to them."
},
{
"chunk": 55,
"story": 4,
"summary": "He tries every way to get his free hand onto the beam across his chest and cannot, and gives up that attempt.",
"predicament": "Someone trapped tries every way to free themselves, and has to admit the first way out is no way at all."
},
{
"chunk": 56,
"story": 4,
"summary": "He sees the muzzle of his own rifle protruding from the wreckage, aimed at the exact centre of his forehead.",
"predicament": "Someone trapped and helpless finds their own weapon pointed straight at them, and can do nothing to turn it away."
},
{
"chunk": 57,
"story": 4,
"summary": "He remembers he left it cocked with a hair trigger; he feels uneasy but not afraid, recalls facing guns before, and looks away.",
"predicament": "Someone proud of their courage faces a danger they made themselves, and tells themselves it is nothing to fear."
},
{
"chunk": 58,
"story": 4,
"summary": "Struggling to free his feet, he fears disturbing the rifle; he recalls a past blunder with a loaded weapon, then distracts himself with the trees and the thought that he faces north, toward his family.",
"predicament": "Someone trapped under a threat they cannot remove tries to distract themselves, and their thoughts drift to their family."
},
{
"chunk": 59,
"story": 4,
"summary": "He decides to wait to be found, but a pain grows in his forehead whenever he closes his eyes; he drifts into childhood memories, and when he cries out he hears fear in his own voice and denies it.",
"predicament": "Someone who has always been fearless hears terror in their own voice for the first time, and lies to themselves about it."
},
{
"chunk": 60,
"story": 4,
"summary": "He cannot look away from the muzzle, beats his hand bloody in delirium, resolves at least to die game, and curses rats that climb near the trigger.",
"predicament": "Someone certain they are going to die resolves at least to die bravely, but cannot bear not knowing when."
},
{
"chunk": 61,
"story": 4,
"summary": "The pain seems to bore through his head; memory, home and glory are erased, and the strong, brave man trembles and screams with terror.",
"predicament": "Someone renowned for courage is broken by waiting for death, until nothing is left of them but fear."
},
{
"chunk": 62,
"story": 4,
"summary": "He works a loose strip of board free and inches it toward the rifle, hoping to push the muzzle aside or deflect the bullet; the effort steadies him, though he is still terrified.",
"predicament": "Someone paralysed by terror finds one small thing to do against the danger, and doing it steadies them."
},
{
"chunk": 63,
"story": 4,
"summary": "The board jams short; terror returns, then he grows calm and deliberately pushes it against the trigger. The rifle was already empty, but the shock kills him.",
"predicament": "Someone who can no longer bear waiting for death decides to face it head on and end the waiting themselves."
},
{
"chunk": 64,
"story": 4,
"summary": "The scout's brother, commanding the picket line, hears a distant crash and notes the time; he is then ordered to advance, since the enemy is thought to have retreated.",
"predicament": "Someone waiting anxiously for a relative sent into danger hears a distant sound, and is ordered forward before knowing what it means."
},
{
"chunk": 65,
"story": 4,
"summary": "The skirmishers pass the ruin; the brother glances at a dust-grey body in the timbers, takes it for an enemy dead a week, and moves on, checking his watch: twenty-two minutes have passed.",
"predicament": "Someone passes the body of their own relative without recognising it, and dismisses it as a long-dead stranger."
}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 66,
"story": 5,
"summary": "The narrator introduces the best officer on the general's staff, a tall, handsome, courteous and fearless newcomer from another regiment, conspicuous in full uniform in action.",
"predicament": "A gifted newcomer joins a close-knit group whose members know nothing of him, and quickly stands out among them."
},
{
"chunk": 67,
"story": 5,
"summary": "The staff grow fond of him but are dismayed that he is vain of his courage: in every battle he refuses to take cover, standing exposed where braver, older men sensibly hide.",
"predicament": "Someone admired by everyone refuses ever to take cover, and the people who like them watch fearing the worst."
},
{
"chunk": 68,
"story": 5,
"summary": "Sent with orders across fire-swept ground, he walks upright instead of running, holding every eye; a fellow officer offers to bet he will be killed before he reaches the ditch.",
"predicament": "Someone walks upright through danger where everyone else runs, while their own comrades lay bets on how soon they will fall."
},
{
"chunk": 69,
"story": 5,
"summary": "He never boasts and turns remonstrance aside with a joke; when the captain who warned him is killed, he tends the body in the middle of a fire-swept road, and his comrades wish he were not a fool.",
"predicament": "Friends try to talk someone out of needless risks, and are laughed off with a joke they have no answer to."
},
{
"chunk": 70,
"story": 5,
"summary": "Sent with a message the long, safe way round, he instead canters into the open field under the enemy's guns; the general shouts to stop him, and the private who tries is killed at once.",
"predicament": "Someone given a safe way to carry out an order takes the deadly way instead, and another dies trying to stop them."
},
{
"chunk": 71,
"story": 5,
"summary": "He rides calmly along the enemy's front; both lines break into a furious battle around him, and he ends up standing alone in the shot-swept space, his horse dead.",
"predicament": "One person's needless display of courage sets off a fight in which many others die, and now leaves them stranded alone."
},
{
"chunk": 72,
"story": 5,
"summary": "Blocked by a gully he could have sheltered in, he will neither advance nor retreat and stands waiting to be killed; both sides stop firing, the enemy honours his body, and a year later the narrator finds a woman's love letter in his pocketbook.",
"predicament": "Someone who could save themselves by taking cover will not be seen to shelter, and stands still until they are killed."
},
{
"chunk": 73,
"story": 5,
"summary": "The letter reveals that a rival had told the woman her lover was seen cowering behind a tree, and that she could bear his death but not his cowardice; the narrator visits her to return it, and she is charming and careless.",
"predicament": "Someone learns that a person they love would rather see them dead than hear them called a coward."
},
{
"chunk": 74,
"story": 5,
"summary": "Seeing the bloodstain, she throws the letter into the fire, saying she cannot bear blood, and asks how he died; the narrator, seeing her beauty and her heartlessness, answers that he was bitten by a snake.",
"predicament": "Someone faces the person whose careless words drove a friend to his death, and must decide whether to tell them."
}
]
@@ -0,0 +1,80 @@
[
{
"chunk": 75,
"story": 6,
"summary": "A general, half-mocking, asks a colonel whether his much-praised young artillery captain would like to put a gun in a narrow notch facing twelve enemy guns; the colonel defends his man, and the general says it is the only place.",
"predicament": "A superior teases someone about the courage they boasted of in a subordinate, then turns the tease into a deadly order."
},
{
"chunk": 76,
"story": 6,
"summary": "Among dead horses and men in the notch, the colonel points out there is room for one gun against twelve; the general ironically says the captain is a battery in himself, and the angry colonel cannot retort.",
"predicament": "Someone sees a superior sending a subordinate into hopeless danger out of spite, and their position forbids them to object."
},
{
"chunk": 77,
"story": 6,
"summary": "The young captain rides up and is told to bring up a gun and engage the twelve; after a silence, he asks with effort whether the enemy guns are near the house.",
"predicament": "Someone ordered to open fire on a target realises, with a sinking heart, exactly where that target is."
},
{
"chunk": 78,
"story": 6,
"summary": "Pale and hoarse, he asks whether the order is imperative; the general rides off without a sign, the mortified colonel nearly arrests him, and then the captain brings up a gun and opens fire.",
"predicament": "Someone ordered to do something that will destroy what they love asks whether it is truly required, and is met with silence."
},
{
"chunk": 79,
"story": 6,
"summary": "A hopeless duel of one gun against twelve begins; the gunners concentrate on the gun in front of the house, shells break into its roof, and the colonel remarks on the captain's reluctance to obey.",
"predicament": "Someone who hesitated before an order now carries it out with terrible thoroughness while the onlookers remember only their hesitation."
},
{
"chunk": 80,
"story": 6,
"summary": "The colonel asks that the captain's hesitation be kept quiet; he refuses the infantry permission to silence the guns, as the general forbade it, and an adjutant begins to hint that the captain is a Southerner.",
"predicament": "Someone begins to suspect that a subordinate's deadly order was never about the enemy at all, but a private grudge."
},
{
"chunk": 81,
"story": 6,
"summary": "The adjutant reveals that the general once camped near the captain's home, there was trouble over the captain's wife and a complaint, and the general was transferred; the indignant colonel goes to withdraw the gun himself.",
"predicament": "Someone learns an order was a superior's revenge on a subordinate's family, and resolves to stop it on their own authority."
},
{
"chunk": 82,
"story": 6,
"summary": "The colonel races down the hill with his retinue to the notch, where the sight appals them.",
"predicament": "Someone hurries to call off an order they now know was malicious, afraid of what they will find when they arrive."
},
{
"chunk": 83,
"story": 6,
"summary": "Four guns lie wrecked in the notch; the half-naked, bloodied survivors work the fifth like madmen, sponging it with their comrades' blood, each man replaced as he falls.",
"predicament": "People carrying out a hopeless order keep at it with mad devotion, each one stepping in as the one before falls."
},
{
"chunk": 84,
"story": 6,
"summary": "Riding over the dead to the gun, the colonel stops the firing; a blackened, fiendish figure turns to him and obeys his signal to withdraw. It is the captain.",
"predicament": "Someone steps into a scene of carnage to end it, and finds the person they sent there changed almost beyond recognition."
},
{
"chunk": 85,
"story": 6,
"summary": "The enemy stops firing too, having long since retreated; the brigade camps on the enemy's ground and the colonel's staff settle comfortably in the shattered plantation house, until an orderly reports something wrong in the cellar.",
"predicament": "Victors settle comfortably into the home they wrecked, until someone uneasy reports a presence hidden below them."
},
{
"chunk": 86,
"story": 6,
"summary": "In the cellar they find a man clasping a dead woman and a dead baby, killed by a shell that broke through the floor above.",
"predicament": "Someone discovers that the destruction they ordered has killed a mother and child hidden where no one had looked."
},
{
"chunk": 87,
"story": 6,
"summary": "The man raises his blackened face and calmly says the house is his, the dead are his wife and child, and he is the captain who shelled it.",
"predicament": "Someone forced to obey an order has destroyed their own home and family with their own hands, and sits with the dead."
}
]
@@ -0,0 +1,50 @@
[
{
"chunk": 88,
"story": 7,
"summary": "After a hard battle, burial parties collect and count the dead; most of the wounded died of neglect while the ground was in dispute, and the enemy's dead are counted more than once for the official report.",
"predicament": "People cleaning up after a disaster treat the dead as numbers, while the wounded who waited for help have quietly died."
},
{
"chunk": 89,
"story": 7,
"summary": "An officer lingers uneasily among the dead at sunset, then, when the burial parties leave, strides purposefully into the forest past the moaning wounded he cannot help.",
"predicament": "Someone walks past the suffering they have no means to help, intent on finding one particular person among the fallen."
},
{
"chunk": 90,
"story": 7,
"summary": "He finds and touches a body that screams; he is a captain whose childhood friend enlisted as a sergeant to stay near him, and who loathes his friend's cynical brother, the regiment's major.",
"predicament": "Someone searching the fallen finds their oldest friend still alive, and the friend's brother is their bitterest enemy."
},
{
"chunk": 91,
"story": 7,
"summary": "That morning the major ordered the captain's company to a deadly position, insultingly suggesting he hand over command; the captain retorted that the major would be better dead, and in the action the sergeant fell.",
"predicament": "An enemy with authority over someone sends them into mortal danger with an insult, and the person they love pays for it."
},
{
"chunk": 92,
"story": 7,
"summary": "The sergeant's belly has been torn open and his clothes ripped apart; the captain cannot understand the wound until he sees a herd of swine feeding on the fallen nearby.",
"predicament": "Someone discovers that a friend left for dead has suffered something far worse than a wound, and turns away sickened."
},
{
"chunk": 93,
"story": 7,
"summary": "The friend is still alive, speechless and screaming when touched, his eyes begging for death; the captain repeats his name until he chokes with emotion.",
"predicament": "Someone watching a friend in hopeless agony sees in their eyes a plea to be put out of their misery."
},
{
"chunk": 94,
"story": 7,
"summary": "Weeping, he walks away and shoots a wounded horse, watching its long death struggle; then he returns, holds the pistol to his friend's forehead and pulls the trigger, but his last cartridge was spent on the horse.",
"predicament": "Someone steels themselves to end a loved one's suffering with their own hand, and finds they have already spent the means."
},
{
"chunk": 95,
"story": 7,
"summary": "He draws his sword and drives it through his friend's heart; the dying man grips the blade, and at that moment the friend's brother steps out of the trees with two stretcher-bearers.",
"predicament": "Someone ends a friend's suffering by the only means left, and is caught in the act by the one person who hates them most."
}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 96,
"story": 8,
"summary": "A captured spy in enemy uniform is questioned by a general in his tent; he jests about his coming execution at dawn and admits why he came.",
"predicament": "A captured prisoner facing death at dawn meets their captor's questions with jokes, as if nothing were at stake."
},
{
"chunk": 97,
"story": 8,
"summary": "The prisoner surveys the bare tent, noting a sabre, pistol and bowie-knife hanging on the pole; the general sends the guard off with a note.",
"predicament": "A prisoner left almost alone with their captor quietly takes stock of their surroundings, and of the weapon within reach."
},
{
"chunk": 98,
"story": 8,
"summary": "The general says he has written the order for the prisoner's execution and offers a chaplain; the prisoner keeps joking and argues that death is nothing to fear.",
"predicament": "Someone condemned to die argues coolly to their captor that death holds no terror, and seems to believe it."
},
{
"chunk": 99,
"story": 8,
"summary": "The prisoner argues there is no such thing as dying, only a last pain; the general, who has a revolver in his lap, shudders and whispers that death is horrible.",
"predicament": "Someone who holds another's life in their hands is shaken by the prisoner's calm and admits to their own fear of death."
},
{
"chunk": 100,
"story": 8,
"summary": "The prisoner scoffs at fear of death as superstition; the general, uneasy, says he would not like to die tonight, then orders the provost-marshal to take the man out and shoot him at once. The prisoner cries out.",
"predicament": "Someone who scoffed at death is suddenly told it is coming now rather than later, and their composure breaks."
},
{
"chunk": 101,
"story": 8,
"summary": "The prisoner begs frantically, insisting he is entitled to be hanged in the morning under military law; the general coldly repeats the order.",
"predicament": "Someone who faced death with jokes begs for a few more hours, clinging to a rule that promised them time."
},
{
"chunk": 102,
"story": 8,
"summary": "Being led out, the prisoner seizes the bowie-knife and leaps on the general; the tent collapses on the struggling men, a sentinel fires, and the camp turns out.",
"predicament": "A cornered person with nothing left to lose seizes a weapon and attacks the one who condemned them."
},
{
"chunk": 103,
"story": 8,
"summary": "The provost-marshal is dead with the knife in his throat and the general badly wounded; the spy cowers in terror while the returning guard tells his story to anyone who will listen; the general orders the spy shot.",
"predicament": "Someone whose desperate attack has only made matters worse cowers in terror, while a bystander seizes the chance to make himself important."
},
{
"chunk": 104,
"story": 8,
"summary": "Officers think the general delirious, but the adjutant produces his written order and has it carried out; the spy is shot begging for his life, and the general dies peacefully a moment later.",
"predicament": "Someone who mocked death dies begging for their life, while the one who dreaded it dies at peace."
}
]
@@ -0,0 +1,86 @@
[
{
"chunk": 105,
"story": 9,
"summary": "A man of Southern sympathies asks the Governor for a commission in the Union army, saying he acts on what he thinks is right, not on what he feels; the Governor, preferring a man with his heart in it, is inclined to refuse.",
"predicament": "Someone asks to serve a cause their heart is not in, and the one who decides doubts they can be trusted."
},
{
"chunk": 106,
"story": 9,
"summary": "He confesses he wants the commission in order to be killed, because his wife has been unfaithful; he does not know the other man's name. The Governor, after a cold insult, grants him a captain's commission.",
"predicament": "Someone betrayed by the person they married seeks out danger in the hope of dying, without knowing who wronged them."
},
{
"chunk": 107,
"story": 9,
"summary": "Alone, the Governor calls it a bad business, opens a book at a sentence about men foolish enough to believe an unfaithful wife's lies, and throws the book into the fire.",
"predicament": "Someone who has just helped a betrayed husband to his death is privately stung by a reminder of their own part in it."
},
{
"chunk": 108,
"story": 9,
"summary": "Months later, during a sluggish campaign, the Governor tours the army with his showy civilian staff, and the soldiers in the trenches curse them as ornaments.",
"predicament": "A comfortable visitor parades through a place of hardship and danger, and the people who suffer there despise them for it."
},
{
"chunk": 109,
"story": 9,
"summary": "A general warns the Governor that only a line of skirmishers lies ahead; the Governor insists on meeting the captain he commissioned and goes forward alone on foot to a company waiting at the front.",
"predicament": "Someone insists on seeking out the person they secretly wronged, walking toward danger they do not understand."
},
{
"chunk": 110,
"story": 9,
"summary": "The captain, aged and scarred almost beyond recognition, salutes the Governor formally but will not shake his hand; the Governor begins to say whose hand it is.",
"predicament": "Someone offers a hand to a person they owe, and receives only a correct and chilly salute in return."
},
{
"chunk": 111,
"story": 9,
"summary": "Firing breaks out from the front; the captain calmly calls his company into line as bullets fly and wounded men stream back.",
"predicament": "A sudden attack interrupts a tense meeting, and a small group must form up at once under fire."
},
{
"chunk": 112,
"story": 9,
"summary": "The captain's calm sing-song commands steady his raw soldiers into acting like veterans; the Governor, torn between pride and terror, runs when the skirmishers fall back.",
"predicament": "A leader's calm voice holds frightened people steady under fire, while an important visitor loses their nerve and runs."
},
{
"chunk": 113,
"story": 9,
"summary": "Fleeing through the jungle, the Governor sprains his ankle leaping a brook, and sits helpless on the ground.",
"predicament": "Someone important who ran from danger is left stranded and helpless, unable to go on, as the fighting comes their way."
},
{
"chunk": 114,
"story": 9,
"summary": "The wounded stream past, deaf to his pleas for help; retreating men fire and walk on, and he is sickened that war is ugly and brutal, nothing like the parades held in his honour.",
"predicament": "Someone who knew only the glamour of a struggle sees its ugly reality close up, and nobody stops to help them."
},
{
"chunk": 115,
"story": 9,
"summary": "The captain's remnant retreats past him; as the enemy converges on the Governor, the captain sees him and orders his few men forward to save the Governor of their State.",
"predicament": "Someone retreating to save their people sees a helpless person about to be overrun, and orders everyone back to rescue them."
},
{
"chunk": 116,
"story": 9,
"summary": "The captain, arm broken, kills attackers with sword and revolver in a savage hand-to-hand fight over the prostrate Governor, until only a handful of his men remain.",
"predicament": "Someone fights savagely to protect a helpless person, not knowing they are defending the one who wronged them."
},
{
"chunk": 117,
"story": 9,
"summary": "Reinforcements arrive; the enemy tramples the last defenders and flees. The Governor wakes unhurt but for his ankle, while a general praises the beautiful fight over rows of dead and wounded.",
"predicament": "Someone saved at a terrible price by others wakes almost unhurt, while their superiors call the slaughter glorious."
},
{
"chunk": 118,
"story": 9,
"summary": "The Governor finds a letter from the captain's wife confessing her sin and naming him, and quietly pockets it; he jokes with the surgeons, accepts a review in his honour, and does not touch the captain's covered body beside him.",
"predicament": "Someone learns that the person who died saving them knew of their betrayal, and quietly hides the proof."
}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 119,
"story": 10,
"summary": "A shabby but striking civilian, coming from the direction of the camp, is halted by a sentinel for his pass while the captain commanding the post looks on.",
"predicament": "A stranger who seems to belong is stopped at a checkpoint, while the one in charge watches and wonders where they have met."
},
{
"chunk": 120,
"story": 10,
"summary": "The civilian shows his pass with a hint of irony and walks on whistling; suddenly the captain draws his revolver and runs after him.",
"predicament": "Someone who has just let a stranger through suddenly realises who they are, and must decide at once what to do."
},
{
"chunk": 121,
"story": 10,
"summary": "The captain holds an isolated, dangerous command and issues passes to loyal locals, with an order that anyone abusing one will be shot; he has recognised the civilian as someone he knows.",
"predicament": "Someone responsible for the safety of many has made a harsh rule, and now recognises the person who has broken it."
},
{
"chunk": 122,
"story": 10,
"summary": "The civilian calmly turns and faces the captain's cocked pistol, offering his pass; the captain says it is forged and names him.",
"predicament": "Someone caught out by the one person who can see through their disguise faces a pistol without showing any fear."
},
{
"chunk": 123,
"story": 10,
"summary": "The man admits he is an enemy spy carrying plans of the fort, offers to submit without resistance if spared the indignity of being marched in at gunpoint, and is refused a handshake but trusted unguarded.",
"predicament": "A captured enemy offers their word to go quietly to their death, and their captor trusts it while refusing their hand."
},
{
"chunk": 124,
"story": 10,
"summary": "Next morning the captain tells the spy he knew him years before as a deserter awaiting execution, guarded one stormy night by an exhausted soldier ordered to kill him if he rose.",
"predicament": "Someone begins to tell the prisoner in their power about a debt they owe them from years ago, which they never repaid."
},
{
"chunk": 125,
"story": 10,
"summary": "The guard fell asleep at his post; the prisoner could have escaped and left him to be shot, but let him sleep and woke him before the relief came. The spy knew the captain all along; the captain weeps.",
"predicament": "Someone whose life was once spared by the person now in their power learns that the other knew them all along and never begged."
},
{
"chunk": 126,
"story": 10,
"summary": "The bugle sounds; the captain confesses that shame kept him from telling the story that might have won the prisoner a pardon, then orders him shot, unbound and unguarded.",
"predicament": "Someone bound by a rule they made must condemn the person who once saved their life, and cannot bring themselves to break it."
},
{
"chunk": 127,
"story": 10,
"summary": "The spy is shot by the firing squad; at the same moment the captain shoots himself, leaving instructions to be buried like the spy, without honours.",
"predicament": "Someone who has done their duty against a person they owed everything cannot live with it, and pays with their own life."
}
]
@@ -0,0 +1,86 @@
[
{
"chunk": 128,
"story": 11,
"summary": "A general tells a battery captain he is not permitted to know anything and must simply open fire on any movement in his front; the captain has his lieutenant witness the order, and salutes with pointed formality as the general rides off into the fog.",
"predicament": "Someone who asked a question is told curtly by a superior that their only job is to obey, and makes sure of a witness."
},
{
"chunk": 129,
"story": 11,
"summary": "After an indecisive day of fighting in rain and mud, the whole army is sullen and does no more than its duty; the general calls it cowardly, his adjutant says it is only cold.",
"predicament": "A whole group does the bare minimum after a miserable day, and the one in charge takes it for cowardice."
},
{
"chunk": 130,
"story": 11,
"summary": "Even the weapons sound feeble; the futile day ends, and a night of discomfort gives way to a morning of apprehension.",
"predicament": "After a day of pointless effort, a dispirited group waits through a miserable night for something worse."
},
{
"chunk": 131,
"story": 11,
"summary": "In thick fog the army senses its lines are badly placed; men and officers talk anxiously, commanders look for their neighbours, and troops dig in of their own accord.",
"predicament": "A large group senses without being told that something is badly wrong with its position, and nobody knows what."
},
{
"chunk": 132,
"story": 11,
"summary": "The captain's battery holds a strong earthwork on what, by his compass, seems to be the extreme left flank of the army, with no troops visible beyond.",
"predicament": "Someone holding a strong position works out that they are on the exposed edge, with nobody else beyond them."
},
{
"chunk": 133,
"story": 11,
"summary": "Alone in the fog, the captain is told by a sergeant that the infantry has been withdrawn, the front is unguarded, and men have heard the enemy's guns moving toward the works.",
"predicament": "Someone left alone to hold a position hears from their people that support has gone and something is approaching."
},
{
"chunk": 134,
"story": 11,
"summary": "He asks who heard the approach, pales slightly, and orders all guns to open fire with grape; meanwhile the general who gave the order tells the division commander the battery is badly commanded.",
"predicament": "Someone ordered to fire on any movement hears of one they suspect is friendly, and obeys the order to the letter."
},
{
"chunk": 135,
"story": 11,
"summary": "The division commander reveals another friendly brigade was sent overnight beyond the battery; cannon fire breaks out on the left, the enemy attacks everywhere, and the battery fires steadily into the fog.",
"predicament": "A superior learns too late that friends were placed where they told a subordinate to fire, just as the firing starts."
},
{
"chunk": 136,
"story": 11,
"summary": "The gunners work idly, firing blind, until a great cheer bursts out of the fog close in front, and they work feverishly.",
"predicament": "People firing blindly at an unseen target suddenly hear it cheering close in front of them, and redouble their efforts."
},
{
"chunk": 137,
"story": 11,
"summary": "Grey figures swarm up the slope into grape and canister and fall in heaps; in dense smoke a few reach the parapet and are bayoneted back into the ditch.",
"predicament": "Defenders in a smoke-filled position cut down a crowd storming toward them without ever seeing clearly who they are."
},
{
"chunk": 138,
"story": 11,
"summary": "The lieutenant urgently protests that they must stop firing; the captain coldly repeats the general's words to him, then shoots dead a man who mounts the parapet waving a flag, and the rest flee.",
"predicament": "Someone who fears their orders are wrong is told by a subordinate that they must stop, and answers with the words used on them."
},
{
"chunk": 139,
"story": 11,
"summary": "The division commander rides in, looks over the parapet and furiously tells the captain he has been fighting their own men; the captain says he knew it, and that the reason was withheld in his orders.",
"predicament": "Someone who knowingly obeyed an order that killed their own side is called to account, and says only that they obeyed."
},
{
"chunk": 140,
"story": 11,
"summary": "The captain refers him to the general who gave the order, but that general is dead; the division commander, sympathetic, begs him for anything that might excuse him, and the captain calls on his lieutenant.",
"predicament": "Someone accused of a terrible act finds that the only person who could confirm their orders is dead."
},
{
"chunk": 141,
"story": 11,
"summary": "The resentful lieutenant says only that he warned the captain and was insulted, and denies knowing of any orders; the captain hears his doom and hands over his sword.",
"predicament": "Someone's last witness, whom they once snubbed, takes their revenge by saying nothing that would clear them."
}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 142,
"story": 12,
"summary": "A captain stands at the head of his company in a vast line of battle awaiting attack; the narrator contrasts the silent order of the front with the endless confusion of the rear.",
"predicament": "Someone stands in the front rank of a huge crowd waiting for a clash that everyone knows is coming."
},
{
"chunk": 143,
"story": 12,
"summary": "Before him lies a peaceful, empty landscape of fields and woods, yet every man in the line knows he is face to face with death.",
"predicament": "Someone looks out at a calm, empty landscape and knows that death is waiting somewhere in it."
},
{
"chunk": 144,
"story": 12,
"summary": "The captain spent two years of the war in a desk post he hated, refusing promotion out of principle; now at last at the front, he commands veterans who think him a shirker, and is too proud to explain.",
"predicament": "Someone kept from the front through no fault of their own is scorned as a shirker, and is too proud to explain."
},
{
"chunk": 145,
"story": 12,
"summary": "He is exultant, eager to prove himself to his men and to a woman at home; then the first enemy shell shrieks toward the line and he dodges and throws up his hands.",
"predicament": "Someone eager to prove their courage flinches at the very first danger, in front of the people they wanted to impress."
},
{
"chunk": 146,
"story": 12,
"summary": "He sees his lieutenant and his men laughing at him and burns with shame; his idea of war collapses, he trembles and chokes, and fears that what he feels is fear.",
"predicament": "Someone who flinched sees their people laughing at them, and fears that the thing they feel is cowardice."
},
{
"chunk": 147,
"story": 12,
"summary": "Battle begins on the right; rifle fire from the woods kills the man beside him, everyone lies down, and he lies sickened beside the yellowing corpse, unable to turn away without facing from his men.",
"predicament": "Someone who imagined glorious death is forced to lie beside a real one, sickened, with nowhere else to look."
},
{
"chunk": 148,
"story": 12,
"summary": "Staring at the silent wood, imagining the hidden guns, he analyses his feelings instead of fighting and groans aloud in despair at what he finds.",
"predicament": "Someone left with nothing to do but wait turns inward to examine their courage, and is devastated by what they find."
},
{
"chunk": 149,
"story": 12,
"summary": "The line is called to attention and waits for an order to advance that does not come; the delay is unbearable, until he looks at his sword, thinks of it as a Roman blade, and grows suddenly calm.",
"predicament": "Someone at the end of their endurance while waiting for danger suddenly sees a way out that would spare them the shame."
},
{
"chunk": 150,
"story": 12,
"summary": "The sergeant behind him sees a bloody blade spring from his back; he has fallen on his sword and dies. A week later the official report lists the losses as one officer, one man.",
"predicament": "Someone who cannot bear to be thought a coward takes their own life before the danger they dreaded ever arrives."
}
]
@@ -0,0 +1,44 @@
[
{
"chunk": 151,
"story": 13,
"summary": "The narrator, a map-maker on a brigade staff, describes an unsociable new aide nobody liked, and his own dangerous work surveying close to enemy lines.",
"predicament": "An aloof newcomer joins a hard-pressed group that does not take to them, while everyone else is too busy to care."
},
{
"chunk": 152,
"story": 13,
"summary": "The new aide asks to come along on a hazardous survey; the narrator gruffly asks in what capacity, and the aide says as a spectator, handing his sword and pistols to his servant. The narrator regrets his rudeness.",
"predicament": "A rude remark to a newcomer is taken so literally that they disarm themselves to go into danger, and the speaker cannot apologise."
},
{
"chunk": 153,
"story": 13,
"summary": "Under fire, the aide sits in the open road with folded arms; his horse is killed, and he stands unarmed before a charging trooper until a lucky bullet drops his attacker. The narrator later apologises.",
"predicament": "Someone disarmed by another's careless words stands in the open facing death with folded arms, too proud to retreat."
},
{
"chunk": 154,
"story": 13,
"summary": "In a later assault the aide is lost and found badly wounded; a captured enemy soldier says he walked straight into their line and, with rifles at his chest, folded his arms and refused to surrender.",
"predicament": "Someone surrounded by enemies with weapons pointed at them is ordered to surrender, and simply refuses."
},
{
"chunk": 155,
"story": 13,
"summary": "At the mess, a stammering quartermaster suggests the aide's folded arms are his way of mastering an urge to run; the narrator angrily defends him. Later the aide learns a giant camp swing and swings higher than anyone dares.",
"predicament": "Someone's extraordinary courage is whispered to be a way of mastering fear, and they go looking for ever greater danger."
},
{
"chunk": 156,
"story": 13,
"summary": "He drives the swing ever higher until he is nearly horizontal; everyone shouts at him to stop, a woman faints, crowds gather, and suddenly the shouting stops.",
"predicament": "Someone pushes a reckless feat further and further while everyone watching begs them to stop, and they will not."
},
{
"chunk": 157,
"story": 13,
"summary": "He lets go of the rope and flies up past a branch, then pitches down with a sickening impact; the hardened soldiers turn sick, and his body lies broken with its arms folded across the breast.",
"predicament": "Someone who faced every danger with defiance meets a senseless end in front of hardened witnesses who cannot bear to look."
}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 158,
"story": 14,
"summary": "A Union private sits against a pine deep in the forest, far outside the lines, looking as if he were asleep.",
"predicament": "Someone alone and far outside the safety of their own side sits brooding, where being caught could mean death."
},
{
"chunk": 159,
"story": 14,
"summary": "He is troubled by the previous night, when he stood sentry at a lonely post in this forest on an over-extended picket line.",
"predicament": "Someone cannot stop turning over what happened on a night they were left alone to keep watch in the dark."
},
{
"chunk": 160,
"story": 14,
"summary": "In the darkness he walked round his tree and lost his bearings, not knowing where the enemy or his own camp lay; then he heard a figure approaching and challenged it.",
"predicament": "Someone on watch loses all sense of direction in the dark, just as a figure comes toward them and will not answer."
},
{
"chunk": 161,
"story": 14,
"summary": "He fired; the whole jumpy picket line fired and ran, but he, not knowing which way to run, stayed put and was praised for bravery. Sure he hit someone but finding no body, he kept quiet.",
"predicament": "Someone is praised for courage they did not show, and keeps quiet about what really happened."
},
{
"chunk": 162,
"story": 14,
"summary": "Granted a pass as a reward for his supposed bravery, he goes back to search for the person he believes he shot, penetrates deep into enemy country, and finally sits down, disappointed.",
"predicament": "Someone who fears they killed a person in the dark goes back alone into danger to find out for certain."
},
{
"chunk": 163,
"story": 14,
"summary": "He reflects that he is in a false position, praised for acts he did not perform, and that telling the truth would sound like a lie; he wishes he could find his man, and falls asleep.",
"predicament": "Someone praised for something they did not do sees that the truth would sound like a lie, and cannot decide what to say."
},
{
"chunk": 164,
"story": 14,
"summary": "He dreams of a happy childhood by a great river with his beloved twin brother, their widowed mother and a mocking-bird whose song filled everything.",
"predicament": "Someone far from home dreams of a lost childhood and the person closest to them, before they were separated."
},
{
"chunk": 165,
"story": 14,
"summary": "In the dream their mother dies, the brothers are parted to feuding relatives on opposite sides of the river, the bird goes with the brother, and their letters stop; he wakes at sunset.",
"predicament": "Someone remembers how they lost touch with the person closest to them when their family was split between enemies."
},
{
"chunk": 166,
"story": 14,
"summary": "Walking back, he hears a mocking-bird and weeps, then finds in a thicket the body of his twin brother in enemy uniform, shot the night before; he never answers roll-call again.",
"predicament": "Someone searching for the stranger they killed in the dark finds it was the person they loved most."
}
]
@@ -0,0 +1,62 @@
[
{
"chunk": 167,
"story": 15,
"summary": "A shack shaped like a face stands on a poor hillside terrace; every day at exactly two o'clock a well-dressed man steps out of its door and checks his watch.",
"predicament": "Someone living in poverty keeps a strange daily ritual with exact punctuality, and nobody around them knows why."
},
{
"chunk": 168,
"story": 15,
"summary": "He walks round a block, staring over his shoulder at a dusty upper window of a factory where nothing ever appears, and returns home, where his wife, a washerwoman who supports them both, helps him in.",
"predicament": "Someone walks past the same empty window every day in hope, while a devoted spouse quietly supports them."
},
{
"chunk": 169,
"story": 15,
"summary": "The man, prematurely aged, is kept neatly dressed by his wife's scanty earnings; thirty years before, the couple were rich, cultivated and apparently happy, and she was proud of him.",
"predicament": "A devoted spouse spends everything keeping up the appearance of the person they married, long after their fortune is gone."
},
{
"chunk": 170,
"story": 15,
"summary": "The husband fell in love with a visiting friend's wife; caught leaving her house at night, he tried to bribe the policeman and gave a false name, with only her jewelled portrait on him.",
"predicament": "Someone caught leaving a lover's home at night takes the blame for a crime rather than let the truth come out."
},
{
"chunk": 171,
"story": 15,
"summary": "His wife finds him in prison under a false name, and in a private interview he bends her to his will; she leaves broken-hearted, keeps his secret, and then disappears herself.",
"predicament": "Someone discovers the person they married has betrayed them, and is persuaded to help keep the betrayal secret."
},
{
"chunk": 172,
"story": 15,
"summary": "He pleads guilty; the lover, ill, twice refuses to testify, tears up her statement, embraces her children in tears, and finally swears she knows nothing and faints.",
"predicament": "Someone must swear to a lie that will send the person they love to prison, or ruin their own family."
},
{
"chunk": 173,
"story": 15,
"summary": "He is sentenced to three years, his lawyer having made no defence; on release his wife meets him at the gate and they go abroad, selling everything.",
"predicament": "Someone who kept silent to protect a lover serves their sentence, and the spouse they betrayed is waiting at the gate."
},
{
"chunk": 174,
"story": 15,
"summary": "Years later they return, broken in fortune and health, living quietly under another name; the wife is endlessly devoted and cheerful, and he is sunk in dejection.",
"predicament": "Someone ruined by a love affair is cared for devotedly by the spouse they betrayed, and remains sunk in misery."
},
{
"chunk": 175,
"story": 15,
"summary": "Wandering one day, he sees his former lover at a window; she cries out, leans out too far and falls to her death, and he sits on the pavement cradling her broken body.",
"predicament": "Someone meets the person they gave up everything for by chance, and the meeting ends in disaster in front of a crowd."
},
{
"chunk": 176,
"story": 15,
"summary": "The lover's husband, understanding everything, leaves for good; the man spends a year in an asylum with his wife caring for him, and they move near the factory so he can make his daily pilgrimage to the window.",
"predicament": "A devoted spouse gives up everything to care for a partner who still mourns someone else every day."
}
]
@@ -0,0 +1,92 @@
[
{
"chunk": 177,
"story": 16,
"summary": "A country schoolmaster lodging at a boarding-house in a resort town for invalids walks home late at dusk by a little-used wooded trail after finishing his report to the trustees.",
"predicament": "Someone walking home alone at dusk by a lonely short cut is tired, and not expecting anything to happen."
},
{
"chunk": 178,
"story": 16,
"summary": "Resting on a log, he hears unseen voices: a man threatening that she and another will suffer if she does not let things stand, and a woman asking whether he would murder them.",
"predicament": "Someone alone in the dark overhears an unseen stranger threatening a woman's life, and cannot see who is speaking."
},
{
"chunk": 179,
"story": 16,
"summary": "He glimpses a tall man in black and a woman in grey; she sinks to her knees in supplication, and when he springs up to intervene, both vanish.",
"predicament": "Someone who decides to intervene in a threat they overheard steps forward, and the people involved simply vanish."
},
{
"chunk": 180,
"story": 16,
"summary": "He half doubts he saw anything; at breakfast two sisters appear, and he recognises the elder's voice as the woman's in the wood.",
"predicament": "Someone who half doubted what they witnessed recognises one of those involved sitting right across from them."
},
{
"chunk": 181,
"story": 16,
"summary": "Afraid of being recognised as an eavesdropper, he leaves; he hears a fine male voice singing an aria that displeases him, and later sees the man in black with the elder sister, and resolves to find out about them honourably.",
"predicament": "Someone who overheard a secret they had no right to hear resolves to find out more, without stooping to spy."
},
{
"chunk": 182,
"story": 16,
"summary": "The handsome man looks him full in the face and he feels hatred and dread; the landlady says the man is the sisters' guardian, their late father's friend, devoted to them and able to pay.",
"predicament": "Everyone praises a charming stranger as devoted and generous, and only one person feels dread in their presence."
},
{
"chunk": 183,
"story": 16,
"summary": "A month passes without incident; then the elder sister is found dead in bed, and the younger turns on the guardian and accuses him of killing her.",
"predicament": "Someone grieving a sudden death turns on the person everyone trusts and accuses them before a crowd."
},
{
"chunk": 184,
"story": 16,
"summary": "The guardian calls her raving and fixes her with his gaze until she falls silent and clings to him weeping; an inquest finds heart disease, and neither of them goes to the funeral.",
"predicament": "Someone who accused a powerful person is silenced by a single look, and ends up clinging to them."
},
{
"chunk": 185,
"story": 16,
"summary": "Overhearing again, the narrator sees the girl and her guardian standing like lovers, and hears her calmly ask him to take her life as he took her sister's, and without delay.",
"predicament": "Someone in the power of a person they believe will kill them asks only that it be done quickly."
},
{
"chunk": 186,
"story": 16,
"summary": "The guardian leaves singing a strange chant; the narrator urges the girl to tell him everything and let him act, since she says the man killed her sister and will kill her.",
"predicament": "Someone who has overheard a person in mortal danger offers to help, and is told there is nothing to be done."
},
{
"chunk": 187,
"story": 16,
"summary": "She refuses, saying she loves the man above all and will deny everything if he goes to the authorities; then she charms him, and he finds himself in love with her.",
"predicament": "Someone trying to rescue a person in danger is told they love their tormentor, and that any accusation will be denied."
},
{
"chunk": 188,
"story": 16,
"summary": "They walk the veranda as she chatters as though bereavement never happened; he feels part of an intrigue and arranges to walk with her the next day, if the guardian is away.",
"predicament": "Someone who meant to rescue another finds themselves drawn into a secret flirtation behind the back of the one they feared."
},
{
"chunk": 189,
"story": 16,
"summary": "On the walk she is capricious and fascinating, jesting and singing by turns, and he falls deeper in love at every step.",
"predicament": "Someone falls deeper under the spell of a person they know is in danger, and loses sight of the danger."
},
{
"chunk": 190,
"story": 16,
"summary": "She leads him to a high cliff-top; the guardian appears, saying he saw them from the road, and her face transfigures with love as she greets him.",
"predicament": "Someone alone with the person they hoped to protect is joined by the very one they feared, and is welcomed with joy."
},
{
"chunk": 191,
"story": 16,
"summary": "The guardian fixes her with his gaze; smiling but terrified, she leaps from the cliff. He coldly says it was her own act, the narrator calls him her murderer, and he walks away singing.",
"predicament": "Someone watches a person they tried to protect go to their death, and the one to blame cannot be touched."
}
]
@@ -0,0 +1,68 @@
[
{
"chunk": 192,
"story": 17,
"summary": "A mining camp is united in condemning a man charged that morning with horse stealing; the rope and the coffin are being prepared before his trial is held.",
"predicament": "Someone accused before the whole community finds the verdict settled and the punishment prepared before any trial."
},
{
"chunk": 193,
"story": 17,
"summary": "The accused had been run out of a neighbouring camp and came here just before its whole population followed; living by cards, he is suspected of robbing the sluice boxes at night.",
"predicament": "Someone driven out of one place finds everyone who drove them out has followed them to the next."
},
{
"chunk": 194,
"story": 17,
"summary": "The camp's leading citizen loudly insists the man is the thief and threatens anyone who disagrees; the saloon-keeper, to keep the citizen's custom, bars the suspect from his gaming table.",
"predicament": "Someone suspected by a powerful and loud accuser is shut out by everyone who depends on the accuser's goodwill."
},
{
"chunk": 195,
"story": 17,
"summary": "The camp decides he must hang; the thefts stop, but he drifts into highway robbery and horse theft, and is caught by his accuser leading away a neighbour's mare at night.",
"predicament": "Someone the whole community has already condemned keeps up their bad habits, and is finally caught in the act by their accuser."
},
{
"chunk": 196,
"story": 17,
"summary": "He is tried and hanged, leaving his whole estate to his captor on condition of a decent burial; a codicil is found in his pocket after death.",
"predicament": "Someone who helped bring a person to the gallows is left everything by them, with a hidden condition attached."
},
{
"chunk": 197,
"story": 17,
"summary": "The codicil gives the estate to anyone who proves in court within five years that he robbed the sluice boxes, otherwise to the executor; the executor laughs and accepts the trust.",
"predicament": "Someone is left a fortune on condition that they spend years proving the innocence of a person they called a thief."
},
{
"chunk": 198,
"story": 17,
"summary": "The estate proves enormous; claimants flood in with evidence, and the executor raises a grand monument praising the dead man's honesty and victimhood.",
"predicament": "Someone who inherits a fortune must now defend the good name of the person they helped hang, against a flood of claimants."
},
{
"chunk": 199,
"story": 17,
"summary": "For five years he fights the claims with lawyers, bribes and false witnesses, corrupting the whole territory, and wins; that night a flood topples the monument and exposes the coffin.",
"predicament": "Someone wins a long, corrupt fight to keep what was left to them, and the victory leaves everything around them rotten."
},
{
"chunk": 200,
"story": 17,
"summary": "The executor, aged and broken by the struggle, now believes absolutely in the dead man's innocence; he has spent the last of the estate on the last witness, and is drawn to the flooded cemetery.",
"predicament": "Someone who argued a lie for years has come to believe it completely, and it has cost them everything they gained."
},
{
"chunk": 201,
"story": 17,
"summary": "Sitting on the fallen monument, he weeps with remorse, believing he caused an innocent man's death by false accusation, since the mare's owner later swore, for a fee, that the theft was arranged.",
"predicament": "Someone who helped hang a person weeps over what they now believe was a false accusation."
},
{
"chunk": 202,
"story": 17,
"summary": "He sees the dead man's ghost panning the dust of the other flooded coffins into its own; in the morning the executor is found dead in the cemetery.",
"predicament": "Someone full of remorse for a person they wronged sees that person's ghost appear before them, still at their old tricks."
}
]
@@ -0,0 +1,50 @@
[
{
"chunk": 203,
"story": 18,
"summary": "The son of a village's leading citizen, breaking a path through fresh snow for his little sister, strikes his foot against something hidden beneath it.",
"predicament": "A child playing in fresh snow stumbles on something hidden beneath it that the grown-ups would rather not explain."
},
{
"chunk": 204,
"story": 18,
"summary": "A great, ugly home for poor old men stands on the hill, endowed by a merchant who gave away half a million dollars, then left for abroad; he has never returned and is now only a name carved over the door.",
"predicament": "Someone who gave away a fortune for the good of others has long since vanished, remembered only as a name."
},
{
"chunk": 205,
"story": 18,
"summary": "The home's superintendent resents the ungrateful old inmates and dreams of running it for prosperous paying guests; the old men live out their lives and are replaced.",
"predicament": "Someone put in charge of a charity resents the very people it was built to help, and wishes they were someone else."
},
{
"chunk": 206,
"story": 18,
"summary": "A ragged old man trudges up the street toward the home on a winter evening, plainly an applicant, since poverty is the qualification.",
"predicament": "Someone old, poor and ragged makes their way through the cold to ask a charity to take them in."
},
{
"chunk": 207,
"story": 18,
"summary": "Avoiding the lamp at the main door, he rings at a side door; the superintendent, jovial and dressed to go out and buy Christmas presents for his children, does not invite him in.",
"predicament": "Someone in need comes humbly to the side door on a festive night, and the person inside is on their way out."
},
{
"chunk": 208,
"story": 18,
"summary": "The superintendent jokes that the trustees have decided his application disagrees with them; the old man cries out in despair.",
"predicament": "Someone who asked for help in their last extremity is turned away with a joke on a night meant for kindness."
},
{
"chunk": 209,
"story": 18,
"summary": "Walking him out, the superintendent pompously explains that under the peculiar circumstances his presence would embarrass the institution; the old man's tracks are already covered by snow.",
"predicament": "Someone refused help is lectured at length on why helping them would embarrass the very institution meant to help."
},
{
"chunk": 210,
"story": 18,
"summary": "The old man wanders off into a vacant lot with nowhere to go; on Christmas morning the chairman's son stumbles on his frozen body. He was the home's founder.",
"predicament": "Someone who built a refuge for others is turned away from it in their own need, and dies in the cold."
}
]
@@ -0,0 +1,98 @@
[
{
"chunk": 211,
"story": 19,
"summary": "In a dusty upper room of an empty house, a body lies under a sheet on a kitchen table beside a candle, an armchair and a reading-stand, with the windows shut and blinds drawn.",
"predicament": "A body has been laid out behind sealed doors in an empty building, as if for some purpose not yet explained."
},
{
"chunk": 212,
"story": 19,
"summary": "A man enters, the door is locked behind him, he finds the windows barred, and he calmly sits down with a book.",
"predicament": "Someone is locked in overnight with a corpse, finds there is no way out, and settles down as if unconcerned."
},
{
"chunk": 213,
"story": 19,
"summary": "The young man, resolute-looking, reads composedly for half an hour, glancing occasionally at the body, then moves the stand aside and returns to his chair.",
"predicament": "Someone determined to show no fear keeps a calm watch over the dead, glancing at it now and then."
},
{
"chunk": 214,
"story": 19,
"summary": "He inspects the face under the sheet, pockets matches and blows out the short candle to save it. Elsewhere, three young doctors over punch discuss whether fear of the dead is inherited and incurable.",
"predicament": "Someone alone with the dead chooses to sit in darkness to save their last light, while others idly debate human fear."
},
{
"chunk": 215,
"story": 19,
"summary": "The host says anyone locked alone all night in the dark with a corpse would crack; the student says he knows a fearless stranger who would take that on for any stake.",
"predicament": "A careless boast about someone else's courage turns into a dare that another person will have to face alone."
},
{
"chunk": 216,
"story": 19,
"summary": "The host accepts the wager, and the third doctor offers to play the corpse. In the dark room, the watcher cannot sleep and tells himself he stays in his chair out of respect for the dead.",
"predicament": "Someone alone in the dark with a corpse cannot sleep, and invents polite reasons for not daring to move."
},
{
"chunk": 217,
"story": 19,
"summary": "He hears a faint sound from the table and holds his breath until he nearly faints; angry, he gropes round the room in the dark and knocks over the stand.",
"predicament": "Someone who prides themselves on courage catches themselves holding their breath in terror at a sound, and is furious."
},
{
"chunk": 218,
"story": 19,
"summary": "He relights the candle, finds nothing changed, checks and bolts the door, sees that only half an hour has passed, and blows the candle out again.",
"predicament": "Someone enduring a terrible night checks that nothing has changed, and finds that almost no time has passed."
},
{
"chunk": 219,
"story": 19,
"summary": "He berates himself as a sceptic who will not lose his bet and self-respect to savage superstition, and then hears soft footsteps approaching behind him. At dawn the host and the student drive toward the house.",
"predicament": "Someone who does not believe the dead can walk hears footsteps coming toward them in the dark."
},
{
"chunk": 220,
"story": 19,
"summary": "The host admits he regrets the wager, which pride pushed him into; the fake corpse is their friend, and as they reach the house a man runs past looking for a doctor.",
"predicament": "People who set up a cruel dare for their own amusement arrive to find it has gone badly wrong."
},
{
"chunk": 221,
"story": 19,
"summary": "A crowd is gathering at the house; the student wants to flee and lie low, but the doctor says a physician may be needed and goes in.",
"predicament": "Someone whose prank has gone wrong must choose between slipping away unseen and going in to face what they caused."
},
{
"chunk": 222,
"story": 19,
"summary": "A wild, white-haired man bursts down the stairs, beating his way through the crowd, and flees into the street; the student calls out the watcher's name and is dragged back.",
"predicament": "Someone sees the person they put up to a dare burst out transformed by terror, and must not be seen to know them."
},
{
"chunk": 223,
"story": 19,
"summary": "Upstairs, a doctor examines the body on the table, face contorted and frothing, pronounces it six hours dead, and calls for the coroner.",
"predicament": "Someone whose prank went wrong looks on while a stranger declares a friend dead, and cannot say a word."
},
{
"chunk": 224,
"story": 19,
"summary": "The police clear the room; the student says the watcher has killed their friend, and the two agree to leave for Europe that afternoon.",
"predicament": "People whose game ended in a death agree without a word of it to flee before anyone connects them."
},
{
"chunk": 225,
"story": 19,
"summary": "Seven years later a white-haired stranger approaches the two in a park, and reveals he is their friend who played the corpse, not the watcher.",
"predicament": "People who ran from a death they caused years ago are confronted by someone they believed long dead."
},
{
"chunk": 226,
"story": 19,
"summary": "He says he rose from the table to frighten the watcher, who died of fright; he changed clothes with the body and was never let out. He is now mad, and the two are gamblers.",
"predicament": "Someone whose cruel joke killed a person was driven mad by it, and blames the friends who left them to it."
}
]
@@ -0,0 +1,56 @@
[
{
"chunk": 227,
"story": 20,
"summary": "An epigraph from an old book of marvels claims that a serpent's eye has a magnetic power that draws its victim forward against his will to his death.",
"predicament": "An old superstition claims that some dangers draw their victims toward them against their own will, however they resist."
},
{
"chunk": 228,
"story": 20,
"summary": "A guest reading the passage scoffs at it, then notices two points of light under his bed that grow brighter and nearer; they are the eyes of a large coiled snake staring at him.",
"predicament": "Someone who has just scoffed at a superstition finds the very thing it describes staring straight at them."
},
{
"chunk": 229,
"story": 20,
"summary": "The guest, a rich, athletic traveller, is staying with a scientist friend who keeps a wing full of reptiles, from which his wife and daughters are excluded.",
"predicament": "Someone staying as a guest with a friend has taken little notice of the dangerous things kept under the same roof."
},
{
"chunk": 230,
"story": 20,
"summary": "Warned that snakes sometimes escape, the guest thinks of ringing for a servant, but does not, lest he be suspected of fear; he cannot tell whether the snake is dangerous.",
"predicament": "Someone faced with a danger they cannot judge will not call for help, in case it looks like fear."
},
{
"chunk": 231,
"story": 20,
"summary": "He resolves to back quietly out of the room, but as he lifts his foot he feels an aversion to retreating, asking himself whether bravery is only pride and whether he will retreat because no one is watching.",
"predicament": "Someone about to retreat sensibly from danger stops, ashamed to back away even when nobody is there to see."
},
{
"chunk": 232,
"story": 20,
"summary": "Declaring he is not so great a coward as to fear seeming afraid to himself, he tries to step back but steps forward instead, again and again, dragging the chair, as the snake's eyes blaze and he hears strange music.",
"predicament": "Someone who refused to back away from danger finds themselves drawn toward it step by step against their will."
},
{
"chunk": 233,
"story": 20,
"summary": "He sees visions, including a crowned serpent with his dead mother's eyes, then falls on his face; the fall breaks the spell, but the thought of the unseen snake is so horrible that he looks again, and is held once more.",
"predicament": "Someone who breaks free of a terror for a moment cannot bear not to look at it, and is caught again."
},
{
"chunk": 234,
"story": 20,
"summary": "He writhes and froths on the floor, inching ever closer to the snake; downstairs, the scientist tells his wife about a new snake-eating snake and scoffs at the superstition of snakes' fascination.",
"predicament": "Someone is being destroyed by their own terror only a floor away from people who laugh at the very idea."
},
{
"chunk": 235,
"story": 20,
"summary": "Terrible cries ring through the house; they find the guest dead with his face under the bed, and the scientist pulls out the snake. It is stuffed, with shoe buttons for eyes.",
"predicament": "Someone dies of terror at a danger that was never real, killed by nothing but their own fear and pride."
}
]
@@ -0,0 +1,122 @@
[
{
"chunk": 236,
"story": 21,
"summary": "A white-haired stranger arrives unnoticed at a mining camp that was deserted two years ago, after the man whose tall tales of gold drew thousands there was shot on the third day.",
"predicament": "Someone arrives alone at a place everyone else abandoned long ago, with a private reason for coming back."
},
{
"chunk": 237,
"story": 21,
"summary": "The abandoned camp is a wreck of skeleton shanties, rotting flumes, torn ground, old boots and bottles.",
"predicament": "Someone looks over the wreckage of a place where thousands once pinned their hopes on a lie."
},
{
"chunk": 238,
"story": 21,
"summary": "The stranger ignores the ruins, goes to a hill, searches among the ground until he finds what he seeks, and stakes out a mining claim according to the local law.",
"predicament": "Someone who has come a long way on a secret tip quietly stakes their claim before anyone else can."
},
{
"chunk": 239,
"story": 21,
"summary": "The hill is the camp's cemetery, which grew as the camp shrank, complete with a hanging tree and a dangling rope; he has staked his claim among the densest graves.",
"predicament": "Someone stakes their fortune on digging up ground where the dead lie buried, and means to go through with it."
},
{
"chunk": 240,
"story": 21,
"summary": "Six years before, he left his fiancée in the East, who told him he never succeeded at anything and would not come back alive.",
"predicament": "Someone leaves the person they love to seek their fortune, sent off with a prediction that they will fail."
},
{
"chunk": 241,
"story": 21,
"summary": "She took up with a gambler who slashed her face; she wrote renouncing the engagement and sent a photograph of her scarred face, but he held to her more firmly out of honour. Then her letters stopped.",
"predicament": "Someone learns the person they are pledged to has been disfigured and released them, and stays faithful out of honour."
},
{
"chunk": 242,
"story": 21,
"summary": "An old drinking friend, now a gravedigger, wrote to him in miners' slang; he tucked the letter away unread for two years, then found and read it.",
"predicament": "Someone discovers years late that a friend once wrote to offer them a secret fortune, and they never answered."
},
{
"chunk": 243,
"story": 21,
"summary": "The letter said the gravedigger had struck a rich gold ledge while digging a grave and hidden it by burying a notorious woman above it; the gravedigger has since drunk himself to death. The claimant calls her a holy terror.",
"predicament": "Someone inherits the secret of a buried fortune from a dead friend, and must dig through a grave to reach it."
},
{
"chunk": 244,
"story": 21,
"summary": "He knows the dead woman only from campfire gossip and a comic obituary; repeating that she was a holy terror, as if to justify himself, he sinks his pick into her grave, and a raven croaks.",
"predicament": "Someone about to desecrate a grave tells themselves the dead person deserved no better, to quiet their conscience."
},
{
"chunk": 245,
"story": 21,
"summary": "Digging feverishly into the night, he stands the coffin on end in the grave and is terrified by his own shadow falling on it, then tries to master his fear by moving his head.",
"predicament": "Someone driven by greed to work alone among the dead at night is startled by their own shadow, and cannot shake the fear."
},
{
"chunk": 246,
"story": 21,
"summary": "He sees that the coffin was buried upside down, the woman face downward.",
"predicament": "Someone alone at night with the dead notices something wrong about the burial that nobody ever explained."
},
{
"chunk": 247,
"story": 21,
"summary": "The absurd discovery overwhelms him with terror; he cannot breathe or move.",
"predicament": "Someone known for their daring is frozen by a terror that is half absurd, and cannot move or cry out."
},
{
"chunk": 248,
"story": 21,
"summary": "His senses return oddly sharp but blind to the coffin; he notices the tree, the rope and the coyotes, smells something sickening, and a raven drops past his face.",
"predicament": "Someone overwhelmed by dread goes numb to its cause and notices everything else, until something startles them back."
},
{
"chunk": 249,
"story": 21,
"summary": "He imagines the rotted corpse behind the lid and, for the first time that day, thinks of his fiancée's photograph, contrasting her beauty with the dead face.",
"predicament": "Someone imagining the horror behind a closed lid finds their thoughts turning to the person they love most."
},
{
"chunk": 250,
"story": 21,
"summary": "He links the disfigured photograph with the dead woman's nickname, which spoke of a disfigurement, and becomes convinced the woman in the coffin is his fiancée.",
"predicament": "Someone becomes convinced, on the flimsiest of links, that the horror before them is the person they love."
},
{
"chunk": 251,
"story": 21,
"summary": "The coffin seems to advance; he strikes the plate with his knife hilt, the rotten lid falls away, and he is face to face with the dead woman, shrieking.",
"predicament": "Someone who can no longer bear imagining the worst forces it open to look, and the sight destroys them."
},
{
"chunk": 252,
"story": 21,
"summary": "Months later, a rich society party passes through; the leader, once a gambler in the camp who narrowly escaped hanging there, shows his wife and friends the hanging tree.",
"predicament": "Someone who escaped disgrace and rose to wealth returns carelessly to the scene of their shady past, with company."
},
{
"chunk": 253,
"story": 21,
"summary": "They pass the picked skeleton of a donkey still tethered beside a miner's kit, and at the tree the millionaire playfully loops the old rope round his neck, horrifying his wife.",
"predicament": "Someone who once nearly met a shameful end makes light of it in front of people who know nothing of their past."
},
{
"chunk": 254,
"story": 21,
"summary": "They find an open grave with two skulls and a broken coffin; a young man jumps in, and the millionaire reads aloud the coffin plate with its coarse epitaph for a local woman.",
"predicament": "Sightseers treat an old tragedy as entertainment, making a show of the remains while the most delicate among them protests."
},
{
"chunk": 255,
"story": 21,
"summary": "The millionaire dismisses a glittering rock as fool's gold; his wife, alone, pockets a wallet from an old coat and finds her own letters and photographs addressed to her former fiancé, and she is found dead, her scarred face white.",
"predicament": "Someone who abandoned a faithful partner long ago stumbles on proof of how they died, and it kills them."
}
]
@@ -0,0 +1,68 @@
[
{
"chunk": 256,
"story": 22,
"summary": "A farm boy, lost at midnight while searching for missing cows, follows a bridle path through a dark forest and sees a light near a deserted house.",
"predicament": "Someone lost alone at night sees a light where no light should be, in a place with a bad name."
},
{
"chunk": 257,
"story": 22,
"summary": "The house is reputed haunted, and the boy himself once helped smash its windows; shaking with fear and guilt, he refuses to turn back and walks past.",
"predicament": "Someone who once mocked what they feared must pass it alone at night, and refuses to turn back."
},
{
"chunk": 258,
"story": 22,
"summary": "Through the window he sees a man staring motionless at a table and believes him dead; he pushes his face into the window, an owl shrieks, the man leaps up and puts out the candle, and the boy flees. The previous morning, a writer finds a friend reading his ghost story on a streetcar.",
"predicament": "Someone frightened out of their wits forces themselves closer to look, and a sudden noise sends everyone fleeing."
},
{
"chunk": 259,
"story": 22,
"summary": "The writer says his friend is ruining his own pleasure by reading a ghost story on a streetcar, likening it to hearing an opera in an office.",
"predicament": "Someone takes offence that a friend enjoys their work casually, and starts to lecture them about it."
},
{
"chunk": 260,
"story": 22,
"summary": "He insists that an author has a right to the reader's undivided attention, growing so vehement that the friend notices his pallor and burning eyes.",
"predicament": "A friend's small grievance swells into fury out of all proportion, and the other begins to wonder what is wrong."
},
{
"chunk": 261,
"story": 22,
"summary": "He says a ghost story must be read alone, at night, by candlelight, if it is to produce the fear the author intends.",
"predicament": "Someone insists that others meet their exact conditions before their work can be fairly judged at all, and grows heated about it."
},
{
"chunk": 262,
"story": 22,
"summary": "The friend, suspecting the writer takes opium, teases him by asking whether the story could really frighten him; the writer says he would not dare to read it alone in a deserted house, and that he has a manuscript that would kill him.",
"predicament": "Someone is told to their face that they lack the courage to face something alone, and the taunt stings."
},
{
"chunk": 263,
"story": 22,
"summary": "Stung, the friend accepts the dare to read the manuscript alone at night in the deserted house. The next day three men and the boy go there, mocking his story, and find a dead body.",
"predicament": "Someone accepts a dare to prove their courage alone at night, and is found dead the next day."
},
{
"chunk": 264,
"story": 22,
"summary": "The men admit the boy was right; one reads the manuscript found by the body, which begins as a suicide note.",
"predicament": "A child nobody believed is proved right in the worst way, and the doubters have to admit it."
},
{
"chunk": 265,
"story": 22,
"summary": "The manuscript is the writer's declaration that he will kill himself at midnight on the anniversary of the day his friend, the house's former owner, killed himself in fulfilment of a vow between them.",
"predicament": "Someone bound by a terrible old pledge to a dead friend feels obliged to follow them into death."
},
{
"chunk": 266,
"story": 22,
"summary": "A postscript tells the reader the writer will come for him after midnight; a relative of the dead friend burns the paper; a newspaper reports the writer has been committed to an asylum after being found sharpening a razor.",
"predicament": "Someone who set out to prove another's courage has frightened them to death, and ends up mad themselves."
}
]
@@ -0,0 +1,44 @@
[
{
"chunk": 267,
"story": 23,
"summary": "Among restless frontier settlers who kept moving west, one silent man stayed on alone in a log house in the forest, living by selling skins and letting his once-cleared land go back to wilderness.",
"predicament": "Someone stays on alone in a place everyone else has left, never smiling and never saying why they stay."
},
{
"chunk": 268,
"story": 23,
"summary": "His cabin has a single window, boarded up for as long as anyone remembers; he looks seventy but is fifty, aged by something besides years. The narrator heard his story from his grandfather.",
"predicament": "Someone aged by an old grief keeps one part of their home shut up for good, and nobody knows the reason."
},
{
"chunk": 269,
"story": 23,
"summary": "He is found dead and buried beside his long-dead wife. Years earlier he had come west young and hopeful with a devoted wife who shared every hardship gladly.",
"predicament": "Someone who came to a hard, lonely place full of hope with the person they loved is left there alone."
},
{
"chunk": 270,
"story": 23,
"summary": "He came home to find his wife delirious with fever, far from any help; after three days of nursing she seemed to die. Numb, he prepares her for burial, fumbling, surprised he cannot weep, telling himself it must be all right.",
"predicament": "Someone alone and far from help watches the person they love die, and is too numb to believe it."
},
{
"chunk": 271,
"story": 23,
"summary": "Stupefied by grief, he finishes laying her out and sits by the body with his head on the table; a wailing cry like a lost child sounds from the woods, and he falls asleep.",
"predicament": "Someone keeping watch alone over a loved one's body is too exhausted by grief to stay awake."
},
{
"chunk": 272,
"story": 23,
"summary": "He wakes in total darkness and hears soft bare footsteps; paralysed with terror, he cannot speak her name. Something crashes against the table and falls, there is a struggle, and when he reaches across, the body is gone.",
"predicament": "Someone alone in the dark with the dead hears something moving near the body, and is too terrified to act."
},
{
"chunk": 273,
"story": 23,
"summary": "Mad with terror he fires his rifle blindly and by the flash sees a panther dragging his wife toward the window; at dawn he finds her throat torn, her fists clenched and a piece of the animal's ear between her teeth. She had not been dead.",
"predicament": "Someone who gave up the person they loved for dead learns too late that they were alive and fighting for their life."
}
]
@@ -0,0 +1,62 @@
[
{
"chunk": 274,
"story": 24,
"summary": "A young heiress writes to her married friend that she is fascinated by a handsome, brilliant, much-travelled man at her seaside resort.",
"predicament": "Someone new to high society finds themselves falling for a charming stranger they know almost nothing about."
},
{
"chunk": 275,
"story": 24,
"summary": "She defends him against her friend's teasing; a gossiping old acquaintance hints he has mysterious connections and is thought a magician, which thrills her and explains his power over her.",
"predicament": "Someone told that the stranger who fascinates them has a dark mystery finds it only makes them more fascinating."
},
{
"chunk": 276,
"story": 24,
"summary": "He calls late while her aunt is out; she is helpless and in love, conscious that she is only a rough mining heiress, and he leaves after a few moments.",
"predicament": "Someone who feels unworthy of the person they love is sure their feelings showed, and that it drove them away."
},
{
"chunk": 277,
"story": 24,
"summary": "The gossip calls again; she likes him because they were childhood playmates in a mining camp, known by humble nicknames.",
"predicament": "Someone now respectable keeps up an old friendship from their humble past, though it could expose where they came from."
},
{
"chunk": 278,
"story": 24,
"summary": "She recalls a third, ragged playmate who scavenged waste rock to feed his mother and defended her; after her father struck it rich they parted, and she heard the boy was later killed.",
"predicament": "Someone who rose to wealth remembers a poor childhood friend who stood up for them, and lost track of."
},
{
"chunk": 279,
"story": 24,
"summary": "She confesses she is ashamed of her origins before the man she loves, and frightened that a relative of her dead playmate staying at the hotel will recognise and expose her.",
"predicament": "Someone hiding a humble past from the person they love fears that an acquaintance from those days will give them away."
},
{
"chunk": 280,
"story": 24,
"summary": "Having seen him from her window, she goes to the beach alone; he asks to sit or walk with her, and she stammers that she would be pleased to do anything.",
"predicament": "Someone who arranged a chance meeting with the person they love is so flustered that they make a fool of themselves."
},
{
"chunk": 281,
"story": 24,
"summary": "He helps her up and will not let go of her hand; her leg has gone to sleep and she sinks, he holds her coolly, and mortified she pushes free and falls in the sand.",
"predicament": "Someone longing to seem graceful before the person they love suffers a small humiliation, and pushes them away in shame."
},
{
"chunk": 282,
"story": 24,
"summary": "She sobs and calls him a thug; he kneels tenderly, declares he has loved her for years, and reveals that he is the ragged childhood playmate she thought dead.",
"predicament": "Someone ashamed of their humble past learns that the person they love shared it, and has loved them since."
},
{
"chunk": 283,
"story": 24,
"summary": "He survived the bullet and came at the gossip's suggestion; the gossip also invented the dark mystery to rouse her curiosity. She is engaged.",
"predicament": "Someone discovers a friend quietly arranged their romance, including the mystery that drew them in."
}
]
@@ -0,0 +1,92 @@
[
{
"chunk": 284,
"story": 25,
"summary": "A young woman with strange, disquieting eyes tells the man she loves that she will never marry him; he angrily demands a reason, and her fixed, emotionless stare tames his anger and makes him shiver.",
"predicament": "Someone who loves a person refuses to marry them and will not say why, while the other demands to know."
},
{
"chunk": 285,
"story": 25,
"summary": "She tells him she is insane, or possessed, and asks him to hear her story; he is disbelieving but deeply disturbed.",
"predicament": "Someone finally tells the person who loves them a dreadful truth about themselves, and the other cannot believe it."
},
{
"chunk": 286,
"story": 25,
"summary": "In a pitch-dark log cabin deep in the forest a woman crouches against the wall clutching a child, watched by something unseen.",
"predicament": "Someone alone in the dark with a child crouches against the wall, knowing they are being watched."
},
{
"chunk": 287,
"story": 25,
"summary": "A frontier woodsman lives devotedly with his young wife and baby in the wilderness; one morning, as he takes his rifle to hunt, she begs him to stay because of a terrible dream.",
"predicament": "Someone begs the person they love not to leave that day because of a dread they cannot explain."
},
{
"chunk": 288,
"story": 25,
"summary": "He laughs off her dream kindly, kisses her and the baby, and goes, closing the door on his happiness forever.",
"predicament": "Someone laughs off a loved one's warning, kindly, and leaves them alone, never to find them the same again."
},
{
"chunk": 289,
"story": 25,
"summary": "He does not return by nightfall; she bars the door, puts a candle in the open window for him and falls asleep, dreaming of a second child with the face of a wild animal, and wakes trembling.",
"predicament": "Someone left alone at night waiting for a partner who has not come home falls asleep and wakes from a nightmare."
},
{
"chunk": 290,
"story": 25,
"summary": "She checks her baby and holds it close, then sees two glowing eyes at the level of her own: a panther at the window.",
"predicament": "Someone alone with a child in the dark suddenly sees a predator's eyes watching them from close by."
},
{
"chunk": 291,
"story": 25,
"summary": "The panther stands at the window watching; paralysed, she sinks to the floor shielding the baby with her body and waits through endless moments for it to spring.",
"predicament": "Someone shielding a child from a danger they cannot fight waits motionless for it to strike, unable to look away."
},
{
"chunk": 292,
"story": 25,
"summary": "The husband returns late, finds the window open, and his wife cowering; she laughs madly and hands him the baby, which she has crushed to death in her embrace. The woman telling the story falls silent.",
"predicament": "Someone comes home too late to find their partner driven mad by terror, and their child dead in their arms."
},
{
"chunk": 293,
"story": 25,
"summary": "The suitor does not understand; she explains she is the second child, born three months after that night at the cost of her mother's life, and asks whether such a person can be sane. He cannot bring himself to take her hand.",
"predicament": "Someone reveals they were born out of a horror that marked them, and asks whether they can ever be like other people."
},
{
"chunk": 294,
"story": 25,
"summary": "The suitor recalls local tales of a panther looking in at windows at night, and theorises that the rumour fed her morbid imagination.",
"predicament": "Someone told an unbelievable story looks for a rational explanation that lets them keep their disbelief."
},
{
"chunk": 295,
"story": 25,
"summary": "He recalls her solitary life and fear of the night and decides she is mad; when he tries to question her she says they cannot marry and flees into the dark woods, and he glimpses shining eyes and runs after her shouting a warning.",
"predicament": "Someone decides the person they love is mad, then sees something in the dark that makes them doubt their own reason."
},
{
"chunk": 296,
"story": 25,
"summary": "He sees her reach her father's door, and no panther. He lives alone in a cottage by the forest, publicly known to have been rejected by her.",
"predicament": "Someone rejected by the person they love lives on alone near them, the refusal known to everyone."
},
{
"chunk": 297,
"story": 25,
"summary": "One night he wakes to gleaming eyes at his open window; terrified but resolute, he raises his revolver and fires.",
"predicament": "Someone woken at night by a terrifying presence at the window has a weapon in hand and a split second to decide."
},
{
"chunk": 298,
"story": 25,
"summary": "He hears a scream, follows a blood trail into the woods, and finds that he has killed not a panther but the woman he loved; her father mourns at her grave for years.",
"predicament": "Someone who fired at a terror in the dark finds they have killed the person they loved."
}
]
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Assemble records.json and groups.json from batches/ and groups_src.py, and check them."""
import collections, glob, json, pathlib
from groups_src import G, STANDING
here = pathlib.Path(__file__).parent
chunks = json.loads((here.parent / 'chunks.json').read_text())
stories = json.loads((here.parent / 'stories.json').read_text())
recs = [r for f in sorted(glob.glob(str(here / 'batches/s*.json'))) for r in json.loads(open(f).read())]
assert [r['chunk'] for r in recs] == list(range(len(chunks))), 'every scene, once, in order'
assert all(chunks[r['chunk']]['story'] == r['story'] for r in recs)
seen = collections.Counter(m for g in G for m in g['members'])
dup = [m for m, n in seen.items() if n > 1]
assert not dup, f'scenes in two groups: {dup}'
by = {r['chunk']: r for r in recs}
out = []
for g in G:
assert 3 <= len(g['options']) <= 5, g['id']
st = sorted({by[m]['story'] for m in g['members']})
stand = collections.Counter(STANDING[by[m]['story']] for m in g['members'])
out.append({
'id': g['id'], 'name': g['name'], 'predicament': g['predicament'],
'sentence': f"A {g['actor']} wants {g['wants']} from {g['counterpart']}.",
'actor': g['actor'], 'wants': g['wants'], 'counterpart': g['counterpart'],
'stories': st, 'story_titles': [stories[s]['title'] for s in st],
'standing': dict(stand), 'stage': stand.most_common(1)[0][0],
'members': [{'chunk': m, 'story': by[m]['story'], 'predicament': by[m]['predicament']} for m in g['members']],
'options': [{'text': t, 'source': s, 'moves': mv,
'in_group': (int(s.split(':')[1]) in g['members']) if s.startswith('scene:') else None}
for t, s, mv in g['options']],
})
unassigned = [r['chunk'] for r in recs if r['chunk'] not in seen]
(here / 'records.json').write_text(json.dumps(recs, indent=1, ensure_ascii=False))
(here / 'groups.json').write_text(json.dumps({'groups': out, 'unassigned': unassigned}, indent=1, ensure_ascii=False))
multi = sum(len(g['stories']) >= 2 for g in out)
print(f'{len(recs)} scenes, {len(out)} groups ({multi} span 2+ stories), '
f'{sum(seen.values())} assigned, {len(unassigned)} unassigned: {unassigned}')
for g in out:
print(f"{g['id']} {len(g['members']):2} scenes {len(g['stories'])} stories stage {g['stage']:6} {g['name']}")
src = collections.Counter(o['source'].split(':')[0] for g in out for o in g['options'])
print('option sources:', dict(src), '| scene options citing a scene outside their group:',
sum(o['in_group'] is False for g in out for o in g['options']))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
"""Hand-built reference grouping of the Bierce predicaments (Claude-written, no local inference).
Each scene is in at most one group; scenes that fit no recurring situation stay unassigned.
Every group spans at least three of the 26 stories, and no story supplies more than half a
group's members.
Options follow DESIGN.md: 3-5 per group, each marked by where it came from --
'archetype' a fixed, setting-free option
'scene:N' what someone in scene N actually did (or refused to do)
'filled' written to fill a gap the scenes leave
`moves` names spine paths and a direction only (+ / - / risk / set). No magnitudes: the model never
chooses a number. Skill labels proposed for this corpus: nerve (kept from Jacobs: holding steady
under fear), wits (reading the signs and thinking coolly in a tight spot).
"""
G = [
dict(id='G01', name='On watch, and your guard slips',
predicament='You were trusted to keep watch through the night, your guard has slipped, and something may already be coming.',
actor='person on watch', wants='to keep everyone safe and the lapse unseen', counterpart='whatever is out in the dark',
members=[0, 1, 3, 160, 271, 289],
options=[
('Own up at once and raise the alarm.', 'filled', {'standing':'-', 'relationships.comrades':'+'}),
('Say nothing and hope nobody noticed.', 'archetype', {'flags.slept_on_watch':'set', 'standing':'risk'}),
('Challenge whatever is out there, loudly.', 'scene:160', {'nerve':'+', 'relationships.comrades':'risk'}),
('Force yourself awake and keep your eyes on the dark.', 'scene:3', {'nerve':'+'})]),
dict(id='G02', name='Duty demands you harm someone you love',
predicament='Your duty requires you to do something that will destroy someone you love, and nobody else can do it for you.',
actor='person under a duty', wants='to do right by both', counterpart='someone they love on the wrong side of it',
members=[5, 6, 7, 9, 77, 78, 121, 126, 127],
options=[
('Do your duty, and tell nobody what it cost you.', 'scene:9', {'standing':'+', 'relationships.loved_one':'-', 'flags.duty_over_love':'set'}),
('Ask whether it is truly required.', 'scene:78', {'relationships.authority':'-', 'standing':'-'}),
('Give them a chance to get clear before you act.', 'scene:6', {'relationships.loved_one':'+', 'standing':'risk'}),
('Refuse, and take the punishment yourself.', 'filled', {'standing':'-', 'relationships.loved_one':'+', 'relationships.authority':'-'})]),
dict(id='G03', name='Told to obey without question',
predicament='You have been told flatly to obey and not to ask why, and you believe the order will get the wrong people hurt.',
actor='subordinate', wants='to do right without defying orders', counterpart='the one giving orders',
members=[76, 79, 128, 134, 138, 139, 152, 153],
options=[
('Obey to the letter, and make sure someone witnessed the order.', 'scene:128', {'relationships.authority':'-', 'flags.order_witnessed':'set', 'standing':'risk'}),
('Obey, and answer anyone who objects with the words used on you.', 'scene:138', {'relationships.subordinates':'-', 'standing':'risk'}),
('Ask plainly whether it is really meant.', 'scene:78', {'relationships.authority':'-', 'nerve':'+'}),
('Refuse, and accept what follows.', 'filled', {'standing':'-', 'relationships.authority':'-', 'nerve':'+'}),
('Hold your tongue and do as you are told.', 'archetype', {'relationships.authority':'+'})]),
dict(id='G04', name='A private score settled through official power',
predicament='Someone with power over you is using it to settle a private score, and it may cost lives besides yours.',
actor='person caught in a grudge', wants='to survive it and stop the harm', counterpart='someone in authority with a grudge',
members=[75, 80, 81, 91, 95, 141],
options=[
('Answer the insult to their face, and go anyway.', 'scene:91', {'nerve':'+', 'relationships.rival':'-', 'standing':'risk'}),
('Say nothing now, and wait for your chance.', 'scene:141', {'wits':'+', 'relationships.rival':'-', 'flags.revenge':'set'}),
('Stop it on your own authority.', 'scene:81', {'nerve':'+', 'relationships.authority':'-', 'standing':'risk'}),
('Keep the gossip quiet and let the matter lie.', 'scene:80', {'relationships.authority':'+', 'standing':'+'})]),
dict(id='G05', name='A stranger who is not what they seem',
predicament='A stranger everyone else accepts seems not quite right to you, and you cannot yet say why.',
actor='watchful person', wants='to know who they really are', counterpart='a charming stranger',
members=[16, 119, 122, 274, 275],
options=[
('Go after them and demand the truth.', 'scene:120', {'nerve':'+', 'relationships.stranger':'-'}),
('Ask around quietly.', 'scene:275', {'wits':'+'}),
('Keep them at arm\'s length.', 'archetype', {'relationships.stranger':'-'}),
('Let yourself be charmed.', 'scene:276', {'relationships.stranger':'+', 'flags.charmed':'set'})]),
dict(id='G06', name='Your heart is on the other side',
predicament='You serve one side while your family, your feelings or your oldest ties belong to the other.',
actor='person of divided loyalty', wants='to be trusted without disowning their own', counterpart='those who doubt them',
members=[2, 105, 164, 165],
options=[
('Tell your family plainly where you stand.', 'scene:2', {'relationships.family':'-', 'standing':'+'}),
('Serve by your head, not your heart, and say so.', 'scene:105', {'standing':'risk', 'relationships.authority':'+'}),
('Keep quiet about where your ties lie.', 'archetype', {'flags.hidden_ties':'set'}),
('Keep in touch with the other side in secret.', 'filled', {'relationships.family':'+', 'standing':'risk', 'flags.divided_loyalty':'set'})]),
dict(id='G07', name='The only way to know is to go and look',
predicament='Nobody knows what waits ahead, and the only way to find out is for someone to walk into it alone.',
actor='volunteer', wants='to learn the truth and bring it back', counterpart='whatever lies ahead',
members=[35, 38, 40, 41, 42, 46, 47, 48, 49, 50, 162],
options=[
('Go yourself, alone.', 'scene:38', {'nerve':'+', 'standing':'+', 'flags.volunteered':'set'}),
('Creep in slowly and read the signs.', 'scene:48', {'wits':'+'}),
('Push on further to be certain.', 'scene:49', {'wits':'+', 'standing':'risk'}),
('Send someone else in.', 'filled', {'relationships.comrades':'-', 'standing':'-'}),
('Hurry back with what you know.', 'archetype', {'relationships.comrades':'+'})]),
dict(id='G08', name='One bold act sweeps everyone into slaughter',
predicament='One person\'s bold act has set everyone around them in motion, and the cost is falling on the rest.',
actor='person caught up in it', wants='to honour the act without dying for it', counterpart='the one who acted, and whoever commands',
members=[44, 45, 70, 71, 115, 116, 117],
options=[
('Rush forward with the rest.', 'scene:44', {'nerve':'+', 'relationships.comrades':'+', 'standing':'risk'}),
('Call everyone back, and bear their anger.', 'scene:45', {'relationships.comrades':'-', 'standing':'+'}),
('Try to stop the one who started it.', 'scene:70', {'nerve':'+', 'standing':'risk'}),
('Hold your place as ordered.', 'archetype', {'relationships.authority':'+', 'relationships.comrades':'-'})]),
dict(id='G09', name='Refusing the safe way out, lest it look like fear',
predicament='There is a safe way out of the danger, and you will not take it because someone might call it fear.',
actor='person proud of their courage', wants='never to be seen afraid', counterpart='the watching crowd, or their own pride',
members=[67, 68, 72, 154, 230, 231],
options=[
('Stand in the open and let them see.', 'scene:67', {'standing':'+', 'nerve':'+', 'flags.reckless':'set'}),
('Refuse to yield, arms folded.', 'scene:154', {'nerve':'+', 'standing':'risk'}),
('Take cover like everyone else.', 'archetype', {'standing':'-'}),
('Call for help, whatever it looks like.', 'scene:230', {'relationships.household':'+', 'standing':'-'})]),
dict(id='G10', name='Your courage is called into question',
predicament='People doubt your courage, and you are ready to risk a great deal to prove them wrong.',
actor='person whose courage is doubted', wants='to be believed brave', counterpart='those who doubt them',
members=[145, 150, 155, 215, 216, 262, 263],
options=[
('Accept the dare.', 'scene:263', {'nerve':'+', 'standing':'risk', 'flags.took_dare':'set'}),
('Explain yourself plainly.', 'filled', {'standing':'+', 'relationships.doubters':'+'}),
('Endure it in silence and wait for a chance to show them.', 'scene:144', {'relationships.doubters':'-', 'standing':'-'}),
('Laugh it off.', 'archetype', {'relationships.doubters':'+'}),
('Stand up for someone else who is being called a coward.', 'scene:155', {'relationships.friend':'+', 'nerve':'+'})]),
dict(id='G11', name='Finding out you are afraid',
predicament='You have always thought yourself brave, and now, where nobody can see, you find that you are afraid.',
actor='person who thought themselves fearless', wants='to master their fear', counterpart='their own nerves',
members=[57, 59, 61, 146, 148, 217, 247],
options=[
('Tell yourself it is nothing, and look away.', 'scene:57', {'nerve':'-'}),
('Name the fear and keep your place.', 'filled', {'nerve':'+', 'standing':'+'}),
('Find one small thing to do about it.', 'scene:62', {'nerve':'+', 'wits':'+'}),
('Cry out for help.', 'scene:59', {'nerve':'-', 'relationships.comrades':'+'})]),
dict(id='G12', name='A gaze you cannot escape',
predicament='Something has fixed its eyes on you, and while you look back you cannot move, think or get away.',
actor='person held by a stare', wants='to break free', counterpart='whatever holds them',
members=[56, 60, 184, 191, 232, 233, 290, 291],
options=[
('Look away and back off slowly.', 'scene:231', {'wits':'+', 'standing':'-'}),
('Hold still and shield whoever is with you.', 'scene:291', {'nerve':'+', 'relationships.family':'+'}),
('Strike out at it.', 'scene:297', {'nerve':'+', 'flags.struck_blind':'set'}),
('Give in to it.', 'scene:184', {'nerve':'-', 'relationships.controller':'+'})]),
dict(id='G13', name='Alone at night with the dead',
predicament='You are alone at night with the dead, and every small sound in the dark begins to seem like movement.',
actor='person keeping vigil', wants='to last until morning', counterpart='the dead, or what seems to be',
members=[212, 213, 218, 219, 245, 249, 250, 251, 272],
options=[
('Sit tight and read until daylight.', 'scene:213', {'nerve':'+'}),
('Light your last candle and look.', 'scene:218', {'nerve':'+', 'wits':'+'}),
('Force it open and face it.', 'scene:251', {'nerve':'+', 'flags.faced_the_dead':'set', 'standing':'risk'}),
('Fire into the dark.', 'scene:273', {'nerve':'+', 'flags.struck_blind':'set'}),
('Call out for someone.', 'archetype', {'nerve':'-', 'relationships.neighbours':'+'})]),
dict(id='G14', name='The one you struck in the dark',
predicament='You struck at someone you could not see clearly, and the one you hurt may be someone you love.',
actor='person who struck in the dark', wants='to know, and to undo it', counterpart='the unseen victim',
members=[65, 136, 137, 161, 166, 297, 298],
options=[
('Go back and search until you find them.', 'scene:162', {'nerve':'+', 'flags.searched':'set'}),
('Keep quiet and let it be forgotten.', 'scene:161', {'standing':'+', 'flags.guilty_secret':'set'}),
('Keep firing until it is over.', 'scene:137', {'standing':'risk', 'flags.struck_blind':'set'}),
('Tell someone what you did.', 'filled', {'standing':'-', 'relationships.comrades':'+'})]),
dict(id='G15', name='Condemned and waiting to die',
predicament='You have been condemned, the end is fixed, and all that is left to decide is how you meet it.',
actor='condemned person', wants='dignity, or a way out', counterpart='those who condemned them',
members=[10, 12, 13, 96, 98, 100, 101, 123],
options=[
('Meet it with jokes.', 'scene:96', {'nerve':'+', 'relationships.captor':'+'}),
('Give your word to go quietly.', 'scene:123', {'standing':'+', 'relationships.captor':'+'}),
('Plead for more time.', 'scene:101', {'standing':'-', 'nerve':'-'}),
('Fix your thoughts on your family.', 'scene:14', {'relationships.family':'+'})]),
dict(id='G16', name='Cornered, looking for a way out',
predicament='You are trapped with almost no room to move, and must find whatever way out is left to you.',
actor='trapped person', wants='to get free', counterpart='whatever holds them',
members=[14, 53, 54, 55, 97, 102],
options=[
('Take stock calmly, as if it were an enemy.', 'scene:53', {'wits':'+'}),
('Seize the nearest weapon and fight.', 'scene:102', {'nerve':'+', 'flags.violent':'set', 'standing':'risk'}),
('Plan a desperate escape and wait for the moment.', 'scene:14', {'wits':'+', 'nerve':'+'}),
('Wait to be found.', 'scene:59', {'relationships.comrades':'+'})]),
dict(id='G17', name='The danger you can only wait for',
predicament='Danger is coming and there is nothing to do but wait for it, and the waiting is worse than the blow.',
actor='person waiting', wants='to hold on', counterpart='the danger that has not yet come',
members=[62, 63, 130, 131, 142, 143, 149],
options=[
('Hold your place and keep your head.', 'archetype', {'nerve':'+'}),
('Keep busy with something small.', 'scene:62', {'nerve':'+', 'wits':'+'}),
('Go and meet it rather than wait.', 'scene:63', {'nerve':'+', 'standing':'risk'}),
('Dig in without being told.', 'scene:131', {'relationships.comrades':'+', 'wits':'+'})]),
dict(id='G18', name='A warning easy to laugh off',
predicament='Someone has warned of a danger that sounds like superstition, and it would be easy to laugh it off.',
actor='sceptic', wants='to carry on as usual', counterpart='the one who warned them',
members=[182, 228, 234, 287, 288, 294],
options=[
('Laugh it off kindly and carry on.', 'scene:288', {'relationships.warner':'-', 'flags.ignored_warning':'set'}),
('Stay home, just this once.', 'scene:287', {'relationships.warner':'+', 'money':'-'}),
('Find a sensible explanation for it.', 'scene:294', {'wits':'+', 'flags.ignored_warning':'set'}),
('Trust your own dread, whatever others say.', 'scene:182', {'relationships.others':'-', 'wits':'+'})]),
dict(id='G19', name='A truth nobody would believe',
predicament='You know something true that sounds impossible, and telling it would make you look a liar or a fool.',
actor='witness', wants='to be believed, or at least not mocked', counterpart='those who would hear it',
members=[8, 163, 180, 264, 285],
options=[
('Say nothing of it.', 'scene:9', {'standing':'+', 'flags.kept_silent':'set'}),
('Tell it anyway and let them laugh.', 'scene:264', {'standing':'-', 'nerve':'+'}),
('Find proof before you speak.', 'scene:162', {'wits':'+', 'nerve':'+'}),
('Tell a smaller story they will believe.', 'filled', {'wits':'+', 'standing':'+'})]),
dict(id='G20', name='Your careless words cost a life',
predicament='Something you said or set going lightly has cost someone their life, and now you must face what you did.',
actor='person at fault', wants='to escape the blame', counterpart='the dead person\'s memory, and everyone else',
members=[74, 107, 220, 221, 224, 266],
options=[
('Go in and face what you caused.', 'scene:221', {'standing':'+', 'nerve':'+', 'flags.faced_it':'set'}),
('Slip away and leave town.', 'scene:224', {'standing':'risk', 'flags.fled':'set'}),
('Quietly hide the proof.', 'scene:118', {'standing':'+', 'flags.guilty_secret':'set'}),
('Confess to the authorities.', 'filled', {'standing':'-', 'relationships.authority':'+'})]),
dict(id='G21', name='Judged before you are heard',
predicament='Everyone around you has already made up their mind about you, and nobody is waiting to hear your side.',
actor='accused person', wants='a fair hearing', counterpart='the people who have judged them',
members=[73, 140, 144, 192, 194, 195],
options=[
('Explain yourself, whatever it costs.', 'filled', {'standing':'risk', 'relationships.accusers':'+'}),
('Refer them to the one who can clear you.', 'scene:140', {'standing':'risk', 'relationships.witness':'risk'}),
('Too proud to explain, endure it.', 'scene:144', {'standing':'-', 'nerve':'+'}),
('Lie low and mend your ways.', 'scene:195', {'standing':'+', 'money':'-'}),
('Get out before they act.', 'scene:193', {'standing':'-', 'flags.run_off':'set'})]),
dict(id='G22', name='A tempting chance with a hidden price',
predicament='You have been handed an easy chance at what you want most, and there may be a price hidden in it.',
actor='person tempted', wants='the prize', counterpart='whoever offered it',
members=[15, 196, 197, 198, 242, 243, 244],
options=[
('Take it and go at once.', 'scene:238', {'money':'+', 'flags.took_the_chance':'set', 'standing':'risk'}),
('Accept, laughing, and deal with the catch later.', 'scene:197', {'money':'+', 'relationships.giver':'+', 'flags.bound_by_terms':'set'}),
('Ask who is offering, and why.', 'filled', {'wits':'+'}),
('Leave it alone.', 'archetype', {'money':'-'})]),
dict(id='G23', name='Turned away when you need help most',
predicament='You need help badly, and the very people who should give it look on, turn away, or explain why they cannot.',
actor='person in need', wants='help', counterpart='those who could give it',
members=[11, 88, 114, 208, 209, 210],
options=[
('Ask plainly, and keep asking.', 'scene:114', {'relationships.helpers':'risk', 'standing':'-'}),
('Tell them who you really are.', 'filled', {'standing':'+', 'relationships.helpers':'risk'}),
('Walk away into the cold.', 'scene:210', {'standing':'-', 'flags.turned_away':'set'}),
('Help yourself, however you can.', 'archetype', {'nerve':'+'})]),
dict(id='G24', name='Watching others suffer from safety',
predicament='Others are suffering in front of you, you are safe yourself, and it is easy to treat it as a show.',
actor='onlooker', wants='to feel as they should, or not at all', counterpart='the people suffering',
members=[30, 31, 33, 89, 108, 253, 254],
options=[
('Get down and help.', 'filled', {'relationships.sufferers':'+', 'money':'-'}),
('Walk on past; there is nothing you can do.', 'scene:89', {'relationships.sufferers':'-'}),
('Make a show of it for your companions.', 'scene:253', {'standing':'+', 'relationships.companions':'+'}),
('Stop the others treating it as a spectacle.', 'scene:254', {'relationships.companions':'-', 'standing':'+'})]),
dict(id='G25', name='Trying to save someone who will not be saved',
predicament='Someone you care about is heading for harm, and refuses every help or reason you offer them.',
actor='friend', wants='to save them', counterpart='someone set on their course',
members=[69, 106, 156, 186, 284],
options=[
('Remonstrate with them, and be laughed off.', 'scene:69', {'relationships.friend':'-'}),
('Threaten to go to the authorities.', 'scene:187', {'relationships.friend':'-', 'nerve':'+'}),
('Give them what they ask, and let them go.', 'scene:106', {'relationships.friend':'+', 'flags.let_them_go':'set'}),
('Shout a warning and run after them.', 'scene:295', {'nerve':'+', 'relationships.friend':'risk'})]),
dict(id='G26', name='Devoted to someone who wronged you',
predicament='The person you love has wronged you badly, and you stand by them anyway, whatever it costs you.',
actor='devoted partner', wants='to keep them', counterpart='the partner who wronged them',
members=[171, 174, 176, 185, 187, 241],
options=[
('Keep their secret, and bear the cost.', 'scene:171', {'relationships.partner':'+', 'money':'-', 'flags.kept_secret':'set'}),
('Hold to your promise out of honour.', 'scene:241', {'standing':'+', 'relationships.partner':'+'}),
('Deny everything to protect them.', 'scene:187', {'relationships.partner':'+', 'relationships.outsider':'-'}),
('Walk away.', 'archetype', {'relationships.partner':'-', 'standing':'+'})]),
dict(id='G27', name='Someone you thought gone for good turns up',
predicament='Someone you thought gone from your life for good is suddenly in front of you, and the past comes with them.',
actor='person with a past', wants='to keep the present intact', counterpart='someone from before',
members=[120, 175, 202, 225, 252, 279, 282],
options=[
('Face them and name them.', 'scene:122', {'nerve':'+', 'relationships.former':'risk'}),
('Pretend you do not know them.', 'filled', {'wits':'+', 'relationships.former':'-'}),
('Make light of the past in front of others.', 'scene:253', {'standing':'risk'}),
('Go to them, whatever the world sees.', 'scene:175', {'relationships.former':'+', 'standing':'-'})]),
dict(id='G28', name='Coming home too late',
predicament='You come back to the people you left behind and find you have arrived too late to help them.',
actor='person returning home', wants='to find them safe', counterpart='the family they left',
members=[34, 86, 87, 270, 292],
options=[
('Stay with them and grieve.', 'scene:87', {'relationships.family':'+', 'standing':'-'}),
('Nurse them yourself, far from help.', 'scene:270', {'relationships.family':'+', 'money':'-'}),
('Go for help at once.', 'filled', {'relationships.neighbours':'+'})]),
dict(id='G29', name='Alone with an old grief',
predicament='You live alone with an old grief, keeping a habit nobody understands, and people have stopped asking why.',
actor='recluse', wants='to keep faith with what they lost', counterpart='neighbours who do not understand',
members=[167, 168, 267, 268, 296],
options=[
('Keep to your ritual.', 'scene:168', {'relationships.neighbours':'-', 'flags.ritual':'set'}),
('Shut away whatever reminds you.', 'scene:268', {'standing':'-'}),
('Tell someone the story at last.', 'filled', {'relationships.neighbours':'+'}),
('Move on, as everyone else has.', 'archetype', {'money':'+', 'relationships.neighbours':'+'})]),
dict(id='G30', name='Lost in the dark, far from help',
predicament='You are lost at night far from anyone who knows you, and the way home looks strange and wrong.',
actor='lost person', wants='to get home', counterpart='the dark country around them',
members=[22, 23, 27, 28, 256, 257],
options=[
('Push on by the stars.', 'scene:256', {'nerve':'+', 'wits':'+'}),
('Lie down and wait for daylight.', 'scene:27', {'nerve':'-'}),
('Go toward the light, whatever it is.', 'scene:31', {'nerve':'+', 'standing':'risk'}),
('Turn back the way you came.', 'archetype', {'wits':'+'})]),
dict(id='G31', name='New to a hardened company',
predicament='You have joined a hardened group that does not know you, and they have already made up their minds about you.',
actor='newcomer', wants='to be accepted', counterpart='the old hands',
members=[37, 66, 151],
options=[
('Prove yourself in the first danger.', 'scene:67', {'nerve':'+', 'relationships.comrades':'+', 'standing':'risk'}),
('Keep to yourself and do your work.', 'scene:151', {'relationships.comrades':'-'}),
('Dress and carry yourself as you please, and let them laugh.', 'scene:37', {'standing':'-', 'nerve':'+'}),
('Win them over with good manners.', 'scene:66', {'relationships.comrades':'+'})]),
]
# Protagonist standing by story, for stage assignment (DESIGN.md "Stages"). Per story, not per
# scene: a coarse first cut, and the one place this grouping uses a judgement not in the text.
STANDING = {0:'low', 1:'high', 2:'low', 3:'middle', 4:'low', 5:'middle', 6:'middle', 7:'middle',
8:'low', 9:'high', 10:'middle', 11:'middle', 12:'middle', 13:'middle', 14:'low',
15:'middle', 16:'middle', 17:'high', 18:'low', 19:'middle', 20:'high', 21:'low',
22:'middle', 23:'low', 24:'high', 25:'middle'}
# Enlisted men, a captive, a child, a pauper and two frontier loners rank low; officers and
# professionals middle; a planter, a governor, a camp's leading citizen, a rich idler and an
# heiress high. Story 15's couple were rich once and are poor now: judged middle.
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Render groups.json + records.json as one self-contained page: REFERENCE.html."""
import html, json, pathlib
here = pathlib.Path(__file__).parent
d = json.loads((here / 'groups.json').read_text())
recs = {r['chunk']: r for r in json.loads((here / 'records.json').read_text())}
stories = json.loads((here.parent / 'stories.json').read_text())
e = html.escape
SRC = {'archetype': 'archetype', 'filled': 'filled gap'}
def src(o):
if o['source'].startswith('scene:'):
n = o['source'].split(':')[1]
return f'from scene {n}' + ('' if o['in_group'] else ' <span class="warn">(outside this group)</span>')
return SRC[o['source']]
def moves(m): return ' '.join(f'<code>{e(k)} {e(v)}</code>' for k, v in m.items())
def scene(c):
r = recs[c]
return (f'<details><summary><b>{c}</b> · <i>{e(stories[r["story"]]["title"])}</i> — {e(r["predicament"])}</summary>'
f'<p class="sum">{e(r["summary"])}</p></details>')
out = []
for g in d['groups']:
out.append(f'''<section><h2>{g["id"]} · {e(g["name"])}</h2>
<p class="pred">{e(g["predicament"])}</p>
<p class="meta">{e(g["sentence"])} · {len(g["members"])} scenes from {len(g["stories"])} stories
({e(", ".join(g["story_titles"]))}) · stage <b>{g["stage"]}</b></p>
<h3>Options</h3><ol>{"".join(f"<li>{e(o['text'])} <span class='src'>{src(o)}</span><br>{moves(o['moves'])}</li>" for o in g["options"])}</ol>
<h3>Scenes</h3>{"".join(scene(m["chunk"]) for m in g["members"])}</section>''')
un = ''.join(scene(c) for c in d['unassigned'])
page = f'''<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Bierce Reference Groups</title>
<style>
:root{{--bg:#fbfaf7;--fg:#1d1d1b;--mute:#6b6760;--line:#e2ded6;--acc:#2f5d8a;--warn:#a4481c}}
@media (prefers-color-scheme:dark){{:root{{--bg:#191917;--fg:#e8e6e1;--mute:#a09c94;--line:#34332f;--acc:#8ab4dc;--warn:#e6915f}}}}
body{{background:var(--bg);color:var(--fg);font:16px/1.5 Georgia,serif;max-width:52rem;margin:0 auto;padding:1rem 16px 4rem}}
h1{{font-size:1.5rem}} h2{{font-size:1.15rem;margin:0 0 .3rem}} h3{{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mute);margin:.9rem 0 .3rem}}
section{{border-top:1px solid var(--line);padding:1.2rem 0}} .pred{{font-size:1.05rem;margin:.2rem 0}}
.meta,.src,.sum{{color:var(--mute);font-size:.88rem}} .warn{{color:var(--warn)}}
code{{font:12px ui-monospace,monospace;border:1px solid var(--line);border-radius:3px;padding:0 .3em;margin-right:.2em}}
li{{margin:.35rem 0}} details{{margin:.2rem 0}} summary{{cursor:pointer}} .sum{{margin:.2rem 0 .5rem 1.2rem}}
</style></head><body>
<h1>Tales of Soldiers and Civilians: reference groups</h1>
<p class="meta">Ambrose Bierce, 1891 (Project Gutenberg #13334). 26 stories, 299 scenes cut by the same rule as <code>chunk.py</code>.
Summaries, predicaments, groups and options written by Claude by reading every scene: no local inference.
A comparison baseline, not a pack. Option moves give a direction only, never a number.</p>
{"".join(out)}
<section><h2>Unassigned ({len(d["unassigned"])})</h2><p class="meta">Scenes that fit no situation recurring in three or more stories.</p>{un}</section>
</body></html>'''
(here / 'REFERENCE.html').write_text(page, encoding='utf-8')
print('wrote', here / 'REFERENCE.html', len(page), 'bytes')
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More