"""Hand-built reference grouping of the Wallingford predicaments (Claude-written, no local inference). The book is a novel in linked episodes, so the unit of recurrence is the SCHEME, not the chapter: a situation seen in three chapters of one scheme has not recurred. ARCS maps every chapter to the scheme it belongs to; ARC_OVERRIDE moves the few scenes that sit in one chapter but belong to the scheme before or after it. Every group spans at least three schemes, has at least five scenes, and no scheme supplies more than half its members. `python3 groups_src.py` prints that check. Each scene is in at most one group; scenes 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 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: charm (the confident front that makes people trust, lend and give credit), cunning (paper tricks, reading a contract, seeing the catch). """ ARCS = { # chapter -> scheme 0: 'tacks', 1: 'tacks', 2: 'tacks', 3: 'tacks', 4: 'tacks', 5: 'tacks', 6: 'tacks', 7: 'insurance', 8: 'insurance', 9: 'insurance', 10: 'insurance', 11: 'recorder', 12: 'recorder', 13: 'recorder', 14: 'recorder', 15: 'recorder', 16: 'recorder', 17: 'traction', 18: 'traction', 19: 'traction', 20: 'traction', 21: 'wheat', 22: 'wheat', 23: 'wheat', 24: 'wheat', 25: 'cigars', 26: 'cigars', 27: 'cigars', } SCHEMES = { 'tacks': 'a company floated on a worthless covered carpet tack, sold to a careful clerk and his lodge friends', 'insurance': 'a fraternal insurance order turned into a stock sale to its own members, widows included', 'recorder': "a real inventor's machine and his friends' savings, sold behind their backs to the monopoly", 'traction': 'a town lured into a land boom by a traction line that was never meant to be built', 'wheat': 'a farmers\' association that holds wheat for $1.50 and is steered into buying the promoter out', 'cigars': "a consolidation of small cigar shops, the promoter's last scheme, which lands him in jail", } # Scenes whose chapter opens or closes on the neighbouring scheme. ARC_OVERRIDE = { **{s: 'tacks' for s in range(78, 83)}, # ch 7 opens with the tack takings: flight, the bucket shop, the pawned jewels **{s: 'insurance' for s in range(120, 123)}, # ch 11 opens with the insurance takings being spent 244: 'traction', 245: 'traction', # ch 21 opens with the traction line being sold for real **{s: 'cigars' for s in range(290, 295)}, # ch 24 closes in the cigar shop where the next scheme begins } G = [ dict(id='G01', name='Keeping up a front of wealth on credit', predicament='You have almost nothing, but you can pass for rich as long as nobody asks you to pay, and the front is what brings the money in.', actor='person with little money', wants='to be taken for rich', counterpart='the people who give them credit', members=[0, 1, 31, 44, 82, 121, 124, 147, 216, 290], options=[ ('Take the finest rooms and let your bearing carry the bill.', 'scene:1', {'standing':'+', 'money':'-', 'flags.living_on_credit':'set'}), ('Furnish a grand household on credit, and entertain the people you need.', 'scene:44', {'charm':'+', 'relationships.investors':'+', 'money':'risk'}), ('Pawn your partner\'s last valuables to keep the front going.', 'scene:82', {'money':'+', 'relationships.partner':'-', 'standing':'+'}), ('Cut back to what you can actually pay for.', 'archetype', {'money':'+', 'standing':'-'})]), dict(id='G02', name='The bill is due and you cannot pay', predicament='Someone wants paying now, you cannot pay, and all you have is the look of a person who could.', actor='person in debt', wants='time', counterpart='a creditor', members=[29, 43, 51, 289, 298, 299, 307, 308], options=[ ('Complain about their service and threaten to take your custom elsewhere.', 'scene:299', {'cunning':'+', 'relationships.creditor':'-', 'standing':'+'}), ('Pocket the bill calmly, as if money were the least of your concerns.', 'scene:308', {'charm':'+', 'flags.bill_outstanding':'set'}), ('Hand them a promise to pay later, and dare them to refuse it.', 'scene:43', {'standing':'+', 'relationships.creditor':'risk', 'flags.bill_outstanding':'set'}), ('Order something more on the same account.', 'scene:51', {'money':'-', 'relationships.creditor':'risk', 'cunning':'+'}), ('Borrow from a friend who thinks you are rich.', 'scene:29', {'money':'+', 'relationships.friend':'risk'})]), dict(id='G03', name='A share in something that does not exist yet', predicament='You are offered a share in a venture that exists only on paper, and everyone says the chance will not come again.', actor='small saver', wants='a share of the profits', counterpart='the promoter', members=[12, 13, 20, 93, 129, 140, 275, 277, 304, 323, 324], options=[ ('Buy in before the chance is gone.', 'scene:140', {'money':'risk', 'relationships.promoter':'+', 'flags.bought_in':'set'}), ('Ask to see the thing itself before you pay a penny.', 'scene:6', {'cunning':'+', 'relationships.promoter':'-'}), ('Take a bigger share than you can afford, in secret.', 'scene:324', {'money':'risk', 'flags.overextended':'set'}), ('Trade your own business for shares instead of cash.', 'scene:304', {'money':'risk', 'standing':'+'}), ('Keep your savings where they are earning.', 'scene:278', {'money':'+', 'relationships.neighbours':'-'})]), dict(id='G04', name='Raising money on a promise you cannot keep', predicament='You are raising money on a promise you know you cannot keep, and the people paying in trust you completely.', actor='promoter', wants='their money', counterpart='the investors', members=[5, 59, 62, 63, 100, 104, 150, 219, 223, 267, 303], options=[ ('Show them plans for the next step, to keep their faith alive.', 'scene:62', {'cunning':'+', 'money':'+', 'relationships.investors':'+'}), ('Borrow a successful rival\'s figures and let them think they are yours.', 'scene:100', {'money':'+', 'flags.misled_investors':'set', 'standing':'risk'}), ('Offer to back the whole venture yourself, knowing you cannot.', 'scene:150', {'charm':'+', 'relationships.investors':'+', 'flags.empty_promise':'set'}), ('Ask for no money now, only free concessions, and promise them a gift.', 'scene:223', {'standing':'+', 'relationships.town':'+'}), ('Tell them plainly what can and cannot be done.', 'filled', {'standing':'-', 'relationships.investors':'risk'})]), dict(id='G05', name='A town caught in a speculative fever', predicament='Everyone around you is suddenly getting rich on paper, and holding back feels like the only way to lose.', actor='townsperson', wants='their share of the boom', counterpart='the neighbours and the promoter', members=[42, 50, 220, 222, 224, 225, 229, 231, 232, 261, 262, 263, 268, 273, 274, 276, 322], options=[ ('Put everything in and ride it up.', 'scene:276', {'money':'risk', 'relationships.neighbours':'+', 'flags.speculated':'set'}), ('Buy in, and sell to the next person at a profit.', 'scene:231', {'money':'+', 'cunning':'+'}), ('Give the promoter whatever he asks, so the town gets its share.', 'scene:224', {'relationships.promoter':'+', 'money':'-', 'standing':'+'}), ('Sell to the eager ones and keep your cash.', 'scene:278', {'money':'+', 'relationships.neighbours':'-'})]), dict(id='G06', name='Sunk in, and asked for more', predicament='Your savings are sunk in a shaky venture, and you must decide whether to put in more, hold on, or get out while you can.', actor='investor', wants='to save what they put in', counterpart='the venture and its promoter', members=[56, 57, 58, 68, 110, 173, 175, 233], options=[ ('Mortgage your home and put in more to save it.', 'scene:68', {'money':'-', 'relationships.partners':'+', 'flags.mortgaged':'set'}), ('Take shares as security for what you are owed.', 'scene:57', {'money':'risk', 'cunning':'+'}), ('Demand your money back, now.', 'scene:110', {'money':'+', 'relationships.promoter':'-'}), ('Cut your losses and let it be sold.', 'scene:175', {'money':'-', 'standing':'-'}), ('Hold on and refuse every offer until it pays.', 'scene:233', {'money':'risk', 'standing':'+'})]), dict(id='G07', name='Working inside a crooked business', predicament='You have realised the business that pays you is crooked, and speaking up would cost you your place.', actor='employee', wants='to keep their living and their conscience', counterpart='the people running it', members=[48, 66, 98, 101, 105, 106, 168, 317], options=[ ('Keep quiet and do the work you are paid for.', 'scene:48', {'money':'+', 'flags.complicit':'set'}), ('Ask for your own pay to be cut, so you are not profiting.', 'scene:66', {'money':'-', 'standing':'+'}), ('Object to the dishonest pitch, to their faces.', 'scene:101', {'relationships.bosses':'-', 'standing':'+'}), ('Stay on, and rebuild it for the people it will fail.', 'scene:106', {'relationships.members':'+', 'money':'risk', 'flags.stayed_to_fix':'set'}), ('Demand a share of the take for your silence.', 'scene:168', {'money':'+', 'cunning':'+', 'flags.complicit':'set', 'relationships.bosses':'risk'})]), dict(id='G08', name='Partners who mean to cheat each other', predicament='You and a partner each hold part of the other\'s fortune, and one of you is already planning to take it all.', actor='partner', wants='their share, or all of it', counterpart='their partner', members=[53, 65, 107, 114, 169, 178, 179, 183, 184, 318, 319, 321, 327], options=[ ('Seem to accept their terms, and get a step ahead of them.', 'scene:169', {'cunning':'+', 'relationships.partner':'-'}), ('Keep them busy and out of the way until the deal is done.', 'scene:178', {'cunning':'+', 'relationships.partner':'-', 'flags.double_crossed':'set'}), ('Go to the law before they can move.', 'scene:327', {'relationships.partner':'-', 'standing':'risk', 'flags.pressed_charges':'set'}), ('Refuse to put your name to what they are doing.', 'scene:65', {'standing':'+', 'relationships.partner':'-'}), ('Let them hold everything in trust for now.', 'scene:321', {'relationships.partner':'+', 'money':'risk'})]), dict(id='G09', name='Out-swindled by someone you trusted', predicament='Someone you took for a safe pair of hands, or an easy mark, has turned out sharper than you and taken everything.', actor='schemer', wants='their money back', counterpart='the one who outwitted them', members=[2, 80, 81, 282, 283, 285, 330], options=[ ('Smile and joke while they enjoy it.', 'scene:330', {'charm':'+', 'standing':'-', 'relationships.rival':'+'}), ('Take it to the law.', 'filled', {'relationships.rival':'-', 'standing':'risk'}), ('Swallow it, and remember how it was done.', 'archetype', {'cunning':'+', 'money':'-'}), ('Blame honesty itself, and swear off straight dealing.', 'scene:287', {'flags.swore_off_honesty':'set', 'relationships.partner':'-'})]), dict(id='G10', name='Let in on the inside', predicament='The person selling to you offers to make you a partner instead, and suddenly you are on the inside of the deal.', actor='mark', wants='a partner\'s share', counterpart='the promoter', members=[24, 26, 33, 87, 88, 130, 305, 306], options=[ ('Accept the partner\'s share, and feel yourself rise.', 'scene:26', {'standing':'+', 'relationships.promoter':'+', 'flags.insider':'set'}), ('Bring your friends in on the good thing.', 'scene:306', {'relationships.friends':'risk', 'standing':'+'}), ('Quietly sell your own piece to the venture above its worth.', 'scene:305', {'money':'+', 'cunning':'+', 'flags.insider':'set'}), ('Haggle hard over what your part is worth.', 'scene:88', {'money':'+', 'relationships.promoter':'-'}), ('Keep it a plain partnership, with no shares to sell.', 'scene:130', {'cunning':'+', 'money':'risk'})]), dict(id='G11', name='Charmed out of your doubts', predicament='You were sure the rich-seeming stranger was not to be trusted, and one warm handshake is melting every doubt.', actor='cautious person', wants='not to be fooled', counterpart='a charming stranger', members=[35, 38, 85, 103, 141, 197, 264, 291], options=[ ('Ask for references, and write to them.', 'scene:37', {'cunning':'+', 'relationships.stranger':'-'}), ('Judge them by how the world treats them: credit everywhere.', 'scene:38', {'relationships.stranger':'+', 'money':'risk'}), ('Let them win you over with a drink and a warm word.', 'scene:141', {'relationships.stranger':'+', 'flags.charmed':'set'}), ('Keep your distance and watch.', 'archetype', {'relationships.stranger':'-'})]), dict(id='G12', name='The lone sceptic', predicament='Everyone around you believes in the newcomer, and you alone think something does not add up.', actor='sceptic', wants='not to be taken in', counterpart='neighbours who believe', members=[6, 18, 89, 139, 204, 218, 278], options=[ ('Demand to see proof before a penny changes hands.', 'scene:6', {'cunning':'+', 'relationships.neighbours':'-'}), ('Ask for payment in advance, and check the money.', 'scene:204', {'money':'+', 'relationships.stranger':'-'}), ('Warn your friends, whatever they think of you.', 'scene:139', {'relationships.friends':'risk', 'standing':'-'}), ('Quietly cash out while they hold on.', 'scene:278', {'money':'+', 'relationships.family':'-'}), ('Give in and join them.', 'scene:141', {'relationships.neighbours':'+', 'flags.charmed':'set'})]), dict(id='G13', name='Cornered by the people you cheated', predicament='The people you took money from have found you out and cornered you, and they are angrier than any law.', actor='schemer', wants='to get away whole', counterpart='the people they cheated', members=[73, 74, 75, 112, 113, 116, 118, 172, 329], options=[ ('Walk through them smiling, as if nothing were wrong.', 'scene:112', {'charm':'+', 'standing':'risk'}), ('Agree to pay it back, and bargain for travelling money.', 'scene:118', {'money':'-', 'cunning':'+'}), ('Brazen it out and ask what they would have done.', 'scene:172', {'cunning':'+', 'relationships.victims':'-'}), ('Beg for mercy.', 'scene:74', {'standing':'-', 'flags.humiliated':'set'})]), dict(id='G14', name='Wiped out', predicament='You have just learned that everything you put in is gone, and people who followed you in have lost it too.', actor='ruined investor', wants='to salvage something', counterpart='those who trusted them', members=[69, 70, 71, 159, 286, 287], options=[ ('Go and tell them yourself, before they hear it elsewhere.', 'scene:71', {'relationships.friends':'+', 'standing':'-'}), ('Keep a brave face in front of everyone.', 'scene:286', {'charm':'+', 'money':'-'}), ('Go back to the steady work you left.', 'filled', {'money':'+', 'standing':'-'}), ('Fight on, whatever it costs.', 'scene:159', {'money':'-', 'standing':'risk'})]), dict(id='G15', name='Small against the giant', predicament='A rival with far more money can undersell you, sue you or simply wait you out, and you must decide how to stand against it.', actor='small operator', wants='to survive', counterpart='a powerful rival', members=[32, 128, 138, 158, 260, 293, 294], options=[ ('Combine with others your size.', 'scene:294', {'relationships.peers':'+', 'money':'risk'}), ('Compete until they have to buy you out.', 'scene:138', {'money':'risk', 'cunning':'+'}), ('Refuse any early offer and hold out for more.', 'scene:32', {'money':'risk', 'standing':'+'}), ('Take the low offer and go.', 'filled', {'money':'+', 'standing':'-'}), ('Fight them in the courts.', 'scene:159', {'money':'-', 'standing':'+'})]), dict(id='G16', name='Courting the rich newcomer', predicament='A rich-seeming stranger has arrived, and you want to be the one he notices, whatever it costs you.', actor='local', wants='the newcomer\'s favour', counterpart='the rich newcomer', members=[41, 123, 125, 142, 194, 205, 209, 210, 255], options=[ ('Hurry to be the first to meet him.', 'scene:194', {'standing':'+', 'relationships.newcomer':'+'}), ('Offer him your land for his great plans.', 'scene:205', {'money':'risk', 'relationships.newcomer':'+'}), ('Bend your rules for him.', 'scene:123', {'relationships.newcomer':'+', 'standing':'-'}), ('Tell everyone how great he is.', 'scene:255', {'standing':'+', 'flags.vouched':'set'}), ('Treat him like anyone else.', 'archetype', {'relationships.newcomer':'-'})]), dict(id='G17', name='A partner who wants to go straight', predicament='Your partner in the business is tired of it, and begs you to take what you have and live decently.', actor='schemer', wants='to keep playing the game', counterpart='a partner who wants out', members=[60, 61, 120, 154, 201, 208, 226, 227, 288, 300], options=[ ('Promise this one is the last.', 'scene:325', {'relationships.partner':'+', 'flags.promised_last':'set'}), ('Laugh it off: there are always more towns.', 'scene:61', {'relationships.partner':'-', 'charm':'+'}), ('Take what you have and settle down.', 'scene:300', {'money':'-', 'relationships.partner':'+', 'standing':'+'}), ('Tell them it is all legal, and leave it there.', 'scene:326', {'relationships.partner':'risk'})]), dict(id='G18', name='Learning what your partner really does', predicament='Someone who shares your life has just understood what their partner\'s business really is, and how it touches them.', actor='partner', wants='to know the truth', counterpart='the schemer', members=[166, 235, 242, 279, 326], options=[ ('Pack and leave.', 'scene:166', {'relationships.partner':'-', 'money':'-'}), ('Beg them to make it honest.', 'scene:242', {'relationships.partner':'risk', 'standing':'+'}), ('Believe the explanation that it is only business.', 'scene:235', {'relationships.partner':'+', 'flags.complicit':'set'}), ('Stand by them, knowing they will not change.', 'scene:333', {'relationships.partner':'+', 'standing':'-'})]), dict(id='G19', name='Buying in with nothing', predicament='You want a full share in the deal but have no money, so you pay with a promise and hope nobody calls it in.', actor='promoter', wants='a full share', counterpart='partners who paid cash', members=[30, 96, 149, 151, 156, 157, 215, 320], options=[ ('Hand over a note instead of cash.', 'scene:96', {'money':'+', 'relationships.partner':'-', 'flags.unpaid_note':'set'}), ('Persuade everyone to pay in less, so your own share is small.', 'scene:149', {'cunning':'+', 'money':'+'}), ('Write a check on a distant bank after hours.', 'scene:156', {'money':'+', 'flags.bad_check':'set', 'standing':'risk'}), ('Postdate the check and hope it is forgotten.', 'scene:320', {'flags.bad_check':'set', 'relationships.partner':'risk'}), ('Pay your share in full.', 'archetype', {'money':'-', 'relationships.partner':'+'})]), dict(id='G20', name='An old accomplice in trouble', predicament='An old friend from your line of work is in trouble with the law and needs money you can barely spare.', actor='schemer', wants='to help without being dragged down', counterpart='an old accomplice', members=[122, 133, 134, 135, 247, 248, 249, 295, 297], options=[ ('Send what they ask, though it leaves you short.', 'scene:122', {'money':'-', 'relationships.old_friend':'+'}), ('Buy them out of trouble, and haggle the price down.', 'scene:134', {'money':'-', 'cunning':'+', 'relationships.old_friend':'+'}), ('Give them your last few dollars.', 'scene:295', {'money':'-', 'relationships.old_friend':'+'}), ('Hide them under a false name.', 'scene:248', {'flags.harbouring':'set', 'standing':'risk', 'relationships.old_friend':'+'}), ('Tell them to keep away from you.', 'archetype', {'relationships.old_friend':'-', 'standing':'+'})]), dict(id='G21', name='The sham turns out to be real', predicament='Something you set up only as a front has turned out to be worth something real, and you could make an honest living from it.', actor='schemer', wants='the biggest payoff', counterpart='someone who wants the real thing', members=[243, 244, 245, 272, 332, 333], options=[ ('Sell it to the one who wants to run it for real.', 'scene:244', {'money':'+', 'standing':'+', 'relationships.town':'+'}), ('Take the honest job you are offered.', 'scene:332', {'money':'+', 'standing':'+', 'flags.went_straight':'set'}), ('Joke about it and go back to your old ways.', 'scene:333', {'charm':'+', 'relationships.partner':'-'}), ('Walk away with what you have taken, as planned.', 'scene:242', {'money':'+', 'relationships.town':'-', 'flags.skipped_town':'set'})]), dict(id='G22', name='Selling out before the crash', predicament='You know the venture is about to collapse, and you can still sell your share to the people who believe in it.', actor='insider', wants='to get their money out first', counterpart='the believers', members=[64, 72, 115, 234, 281, 325], options=[ ('Sell your own shares quietly while the others hold theirs.', 'scene:64', {'money':'+', 'relationships.investors':'-', 'flags.sold_out':'set'}), ('Squeeze one last payment from them before you go.', 'scene:72', {'money':'+', 'standing':'risk'}), ('Sell what you promised others, and slip away with the money.', 'scene:325', {'money':'+', 'relationships.partner':'-'}), ('Stay and see it through with them.', 'filled', {'money':'-', 'relationships.investors':'+'})]), ] # Protagonist standing for stage assignment (DESIGN.md "Stages"). Keyed by chapter because build.py # expects that, but judged per scheme: every chapter of a scheme gets the scheme's value. The swindler # arrives nearly penniless among small savers (tacks), rises through two mid-sized schemes, is the # great man of a town and then of a farmers' movement, and ends broke among cigar-shop keepers. SCHEME_STANDING = {'tacks': 'low', 'insurance': 'middle', 'recorder': 'middle', 'traction': 'high', 'wheat': 'high', 'cigars': 'low'} STANDING = {ch: SCHEME_STANDING[s] for ch, s in ARCS.items()} def scheme_of(chunk, story): return ARC_OVERRIDE.get(chunk, ARCS[story]) if __name__ == '__main__': # Scheme-level recurrence: >= 5 scenes, >= 3 schemes, no scheme over 50% of the group. import collections, json, pathlib chunks = json.loads((pathlib.Path(__file__).parent.parent / 'chunks.json').read_text()) ok = 0 for g in G: sc = collections.Counter(scheme_of(m, chunks[m]['story']) for m in g['members']) top = max(sc.values()) / len(g['members']) passed = len(g['members']) >= 5 and len(sc) >= 3 and top <= 0.5 ok += passed print(f"{'PASS' if passed else 'FAIL'} {g['id']} {len(g['members']):2} scenes {len(sc)} schemes " f"top {top:4.0%} {dict(sc)} {g['name']}") print(f'{ok} of {len(G)} groups pass the scheme-level rule')