rs_rules
The case behind *Red String Over the Atlantic*, and the machinery that decides whether a board full of string is a proof or a guess.
rs_rules
The case behind Red String Over the Atlantic, and the machinery that decides whether a board full of string is a proof or a guess.
It holds the eight people, the twenty-seven pieces of evidence and every claim printed, typed or spoken on them; which of those claims are lies; and who actually killed Alastair Kettering. It draws nothing and owns no screen.
Using it
game/boot.module installs it first, so every other slice can reach it — at
call time, never at require time:
local R = Core.need("rules")
if not R then return end
| Call | Gives you |
|---|---|
R.people() / R.person(id) | The cast. name, short, title, role, cabin, blurb. |
R.cards() / R.card(id) | All 27 evidence cards, or one. |
R.availableAt(leg, station, source) | What is gatherable there, this leg. source is "examine" or "interview:<personId>". |
R.isStale(id, leg) / R.staleWhy(id) | Whether the paper has died, and the line that says why. |
R.contradicts(a, b) -> ok, why | The predicate. why is always a sentence, whichever way it goes. |
R.contradictionDetail(a, b) | The two clashing claims, their quotes, and the question they both answer. |
R.evaluateLink(a, b, opts) | contradicts plus do you hold both and is either stale. What the board should call when a pin goes in. |
R.partnersFor(id, opts) | Every held card that would hold a string to this one. |
R.verifyChain(links, opts) -> ok, report | The whole board. |
R.accuse(personId, links, opts) -> verdict | Name somebody. Three endings. |
R.advanceLeg() / R.legReport(leg) | Turn the crossing; what opened and what died. |
R.revalidateLinks(leg) | Re-read every hanging string against the leg it now hangs in. Runs automatically on rs.leg. |
R.intendedChain() / R.solution() | The four links the case was built to close, and the truth. Ending card only. |
The rule
Two cards contradict when some claim on one and some claim on the other share
a subject and a topic but differ in value — same person, same
question, two answers.
Values are normalised through a synonym table first, so two authors spelling
one place differently never manufacture a lie. Rules.EXCEPTIONS covers the
places where the ship's own paperwork disagrees with itself honestly: the
purser moved Mrs Vaudrey from Cabin 9 to Cabin 14 before departure, so the two
manifest sheets differ and neither is lying. Pinning those two together is
refused, with the purser's amendment typed on the slip.
The chain
verifyChain walks six clauses in order and stops at the first that fails.
The failing clause's sentence becomes report.why, and that is what the
ending card prints.
- links — there is string on the board at all.
- held — every endpoint is a real card, and one you hold.
- contradiction — every string joins two answers that cannot both be true.
- fresh — no string hangs on paper that has gone stale this leg.
- connected — the board is one argument, not several.
- cycle — the argument closes on itself, and the loop runs through
exactly one person's own account (an
accusablecard).
accuse(personId, links) runs that, then:
| Ending | When |
|---|---|
proven | The chain holds, the loop names the man you named, and he did it. |
wrong | The chain holds and closes through the man you named — and he is innocent. An innocent goes down the mast between two policemen. |
walks | The chain fails a clause, or closes through somebody other than the man you named. verdict.failed is the clause id and verdict.line says which. |
Staleness
staleAfter = 2 means good through leg 2, dead paper from leg 3. Two cards
die on the crossing: Plate 14 (MacKaye fogs it developing a second set) and
MacKaye's ticket stub (it goes into the salon fire). A string already
hanging on one of them is re-read and killed the moment the leg turns —
rs_rules subscribes to rs.leg in install and emits rs.links when
anything changed.
Extending
The topic vocabulary in the spec (whereabouts@<hhmm>, door@<hhmm>,
possession:<item>, saw:<p>@<hhmm>, time_of_death, cabin_of:<p>,
wire_sender, wire_recipient, ticket_class, boarded_at) is extended here
with signatory, cargo_loaded and cormorant_role. Rules.question(claim)
turns any of them into the prose a player reads; add a branch there when you
add a topic, or it falls back to the raw token.
Scoped to this part · feeds back into the world's score.