---
format:
html:
toc: true
toc-depth: 2
theme: {dark: darkly, light: cosmo}
number-sections: false
number-depth: 0
code-fold: false
code-tools: true
highlight-style: nord
---
<style></style><style>.printedClojure .sourceCode {
background-color: transparent;
border-style: none;
}
</style><style>.clay-limit-image-width .clay-image {max-width: 100%}
.clay-side-by-side .sourceCode {margin: 0}
.clay-side-by-side {margin: 1em 0}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" type="text/javascript"></script><script src="https://code.jquery.com/ui/1.13.1/jquery-ui.min.js" type="text/javascript"></script>
```{=html}
<div style="display:none;"><style>/* ---------------------------------------------------------------
Fedoras — one palette, two schemes.
Dark is the default. Quarto puts .quarto-light on the body when the
reader flips the toggle, and everything below is written against
these variables, so a scheme change is nine lines rather than three
hundred.
--------------------------------------------------------------- */
:root {
--paper: #1b1d21; /* background */
--ink: #e6e4df; /* text */
--ink-soft: #a9a49b; /* secondary text */
--rule: rgba(230, 228, 223, 0.22);
--panel: rgba(230, 228, 223, 0.05);
--violet: #9d8dff; /* apparatus, epigraphs */
--gold: #d0a44c; /* beats, commits */
--coral: #e2707c; /* moderation, rewrites */
--green: #6fcf9b; /* the Helps */
--blurple: #8b95f0; /* Discord */
--violet-rgb: 157, 141, 255;
--gold-rgb: 208, 164, 76;
--coral-rgb: 226, 112, 124;
--green-rgb: 111, 207, 155;
--blurple-rgb: 139, 149, 240;
--ink-rgb: 168, 164, 156;
--panel-hard: #101215;
}
body.quarto-light {
--paper: #faf8f4;
--ink: #1c1f24;
--ink-soft: #6b665e;
--rule: rgba(28, 31, 36, 0.22);
--panel: rgba(28, 31, 36, 0.05);
--violet: #6c5ce7;
--gold: #b0863a;
--coral: #e04f5f;
--green: #2ea078;
--blurple: #5865f2;
--violet-rgb: 108, 92, 231;
--gold-rgb: 176, 134, 58;
--coral-rgb: 224, 79, 95;
--green-rgb: 46, 160, 120;
--blurple-rgb: 88, 101, 242;
--ink-rgb: 128, 128, 128;
--panel-hard: #14161a;
}
/* ---------------------------------------------------------------
Fedoras — screenplay typography for the HTML edition.
Loaded by fedoras.screenplay/assets. Edit here, rebuild, done.
PDF styling lives in notebooks/tex/fedoras-preamble.tex instead.
--------------------------------------------------------------- */
.screenplay {
font-family: "Courier Prime", "Courier New", ui-monospace, monospace;
font-size: 1rem;
line-height: 1.35;
max-width: 62ch;
margin: 1.6rem 0 2.2rem 0;
}
.screenplay .slug {
font-weight: 700;
letter-spacing: 0.04em;
margin: 1.6rem 0 0.8rem 0;
}
.screenplay .action {
margin: 0 0 0.85rem 0;
}
.action{
white-space: pre-line;
}
.screenplay .action.beat {
margin-bottom: 0.35rem;
}
.screenplay .dialogue {
margin: 0 0 0.95rem 0;
}
.screenplay .character {
margin: 0 0 0 12ch;
font-weight: 700;
letter-spacing: 0.06em;
}
.screenplay .paren {
margin: 0 0 0 9ch;
font-style: italic;
opacity: 0.85;
}
.screenplay .lines {
margin-left: 6ch;
max-width: 50ch;
}
.screenplay .line {
margin: 0;
}
.screenplay .transition {
margin: 1.1rem 0 1.1rem 0;
text-align: right;
font-weight: 700;
letter-spacing: 0.08em;
}
.screenplay .direction {
font-style: italic;
opacity: 0.85;
margin: 0 0 0.9rem 0;
}
/* In-world text: banners, signs, whiteboards. */
.screenplay .sign {
border: 1px solid currentColor;
padding: 0.7rem 1rem;
margin: 0.9rem 0 1.1rem 4ch;
max-width: 46ch;
text-align: center;
letter-spacing: 0.05em;
}
.screenplay .sign p {
margin: 0.15rem 0;
}
.screenplay .whiteboard {
text-align: left;
}
.screenplay .whiteboard-title {
font-weight: 700;
text-transform: uppercase;
margin-bottom: 0.4rem !important;
}
.screenplay .whiteboard ol {
margin: 0;
padding-left: 2.2ch;
}
/* --- The Fedora Handbook epigraphs ------------------------------- */
.handbook {
max-width: 54ch;
margin: 1.4rem 0 2rem 2ch;
padding-left: 1.1rem;
border-left: 3px solid var(--violet);
font-style: italic;
}
.handbook .rule-number {
font-style: normal;
font-variant: small-caps;
letter-spacing: 0.08em;
opacity: 0.75;
margin-bottom: 0.25rem;
}
.handbook .rule-text {
margin: 0;
}
/* --- Drafting apparatus (hidden in the reading copy) -------------- */
.beats {
border: 1px dashed var(--gold);
background: rgba(var(--gold-rgb), 0.07);
padding: 0.6rem 0.9rem;
margin: 1.1rem 0;
max-width: 62ch;
font-size: 0.88rem;
}
.beats-title {
font-variant: small-caps;
letter-spacing: 0.1em;
font-weight: 700;
margin: 0 0 0.35rem 0;
opacity: 0.8;
}
.beats ul {
margin: 0;
padding-left: 1.4rem;
}
.beats.note {
border-color: var(--blurple);
background: rgba(var(--blurple-rgb), 0.07);
}
.status {
display: inline-block;
font-family: ui-monospace, monospace;
font-size: 0.7rem;
letter-spacing: 0.14em;
padding: 0.15rem 0.5rem;
border-radius: 3px;
margin: 0 0 0.8rem 0;
}
.status-outline { background: var(--ink-soft); color: #fff; }
.status-draft { background: var(--gold); color: #fff; }
.status-locked { background: var(--green); color: #fff; }
/* --- Live (editable) code cells ---------------------------------- */
.live-cell {
border: 1px solid rgba(var(--ink-rgb), 0.4);
border-radius: 5px;
padding: 0.7rem 0.8rem;
margin: 1.2rem 0;
max-width: 68ch;
}
.live-label {
font-size: 0.78rem;
letter-spacing: 0.09em;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 0.45rem;
}
.live-src {
width: 100%;
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.9rem;
line-height: 1.4;
padding: 0.5rem;
border: 1px solid rgba(var(--ink-rgb), 0.35);
border-radius: 4px;
background: rgba(var(--ink-rgb), 0.06);
resize: vertical;
}
.live-bar {
margin-top: 0.5rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.live-run {
font: inherit;
font-size: 0.85rem;
padding: 0.25rem 0.9rem;
border-radius: 4px;
border: 1px solid rgba(var(--ink-rgb), 0.5);
background: rgba(var(--violet-rgb), 0.15);
cursor: pointer;
}
.live-run:hover { background: rgba(var(--violet-rgb), 0.3); }
.live-out {
margin: 0.5rem 0 0 0;
padding: 0.45rem 0.6rem;
min-height: 1.6rem;
font-size: 0.88rem;
white-space: pre-wrap;
border-left: 3px solid rgba(var(--violet-rgb), 0.6);
background: rgba(var(--ink-rgb), 0.06);
}
/* --- Front matter ------------------------------------------------- */
.title-page {
text-align: center;
margin: 3rem 0 2rem 0;
}
.title-page .title {
font-family: "Courier Prime", "Courier New", monospace;
font-size: 3.2rem;
letter-spacing: 0.22em;
margin: 0;
}
.title-page .subtitle {
font-family: "Courier Prime", "Courier New", monospace;
letter-spacing: 0.1em;
margin-top: 0.8rem;
opacity: 0.85;
}
h1.sidebar-title,
h2.sidebar-title {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.personae {
max-width: 66ch;
}
.personae h3 {
font-variant: small-caps;
letter-spacing: 0.1em;
margin-top: 2rem;
border-bottom: 1px solid rgba(var(--ink-rgb), 0.35);
padding-bottom: 0.2rem;
}
.personae .who {
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700;
letter-spacing: 0.06em;
margin: 1rem 0 0.1rem 0;
}
.personae .what {
margin: 0 0 0.2rem 0;
font-style: italic;
opacity: 0.8;
}
.personae .desc {
margin: 0.15rem 0 0 0;
max-width: 60ch;
}
/* --- The reader's protagonist ------------------------------------ */
.pc-name {
font-weight: inherit;
border-bottom: 1px dotted rgba(var(--violet-rgb), 0.55);
}
.screenplay .slug,
.screenplay .transition,
.screenplay .character { text-transform: uppercase; }
.name-field {
border: 1px solid rgba(var(--violet-rgb), 0.55);
border-radius: 5px;
padding: 0.9rem 1rem;
margin: 1.6rem auto;
max-width: 60ch;
}
.name-label { margin: 0 0 0.6rem 0; }
.name-inputs { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.name-inputs input {
font: inherit;
padding: 0.3rem 0.6rem;
border: 1px solid rgba(var(--ink-rgb), 0.45);
border-radius: 4px;
background: rgba(var(--ink-rgb), 0.06);
max-width: 14ch;
}
.name-note {
font-size: 0.82rem;
opacity: 0.7;
margin: 0.6rem 0 0 0;
}
.name-label,
.name-confirm {
font-size: 1.2rem;
}
.name-inputs input,
.name-inputs button {
font: inherit;
font-size: 1.2rem;
}
/* --- Artifacts ---------------------------------------------------- */
.artifact {
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.88rem;
line-height: 1.45;
max-width: 66ch;
margin: 1.4rem 0;
border-radius: 5px;
overflow: hidden;
}
.artifact p { margin: 0.1rem 0; }
/* Discord */
.discord { border: 1px solid rgba(var(--blurple-rgb), 0.5); }
.discord-head {
background: rgba(var(--blurple-rgb), 0.14);
padding: 0.4rem 0.8rem;
display: flex; gap: 0.8rem;
}
.discord-server { font-weight: 700; letter-spacing: 0.06em; }
.discord-channel { opacity: 0.75; }
.discord-body { padding: 0.7rem 0.8rem; }
.discord-msg { margin-bottom: 0.7rem; }
.discord-who { font-weight: 700; }
.discord-role {
font-size: 0.7rem; letter-spacing: 0.08em;
background: rgba(var(--violet-rgb), 0.25);
padding: 0.05rem 0.4rem; border-radius: 3px; margin-left: 0.4rem;
}
.discord-at { opacity: 0.55; margin-left: 0.5rem; font-size: 0.78rem; }
.discord-reacts { margin-top: 0.2rem !important; }
.react {
display: inline-block; font-size: 0.75rem;
border: 1px solid rgba(var(--ink-rgb),0.35); border-radius: 10px;
padding: 0 0.45rem; margin-right: 0.3rem;
}
/* Moderation log */
.modlog {
border-left: 3px solid var(--coral);
background: rgba(var(--coral-rgb), 0.08);
padding: 0.6rem 0.9rem;
font-size: 0.82rem;
letter-spacing: 0.03em;
}
/* Git */
.commit {
border: 1px solid rgba(var(--ink-rgb), 0.4);
padding: 0.7rem 0.9rem;
background: rgba(var(--ink-rgb), 0.05);
}
.commit-hash { color: var(--gold); font-weight: 700; }
.commit-meta { opacity: 0.7; }
.commit-msg { margin-top: 0.5rem; padding-left: 1.5rem; }
/* Terminal */
.terminal {
background: var(--panel-hard);
color: var(--ink);
padding: 0.7rem 0.9rem;
border: 1px solid rgba(var(--ink-rgb),0.35);
}
.term-in { color: var(--green); }
.term-in::before { content: ""; }
.term-out { opacity: 0.9; }
/* AI */
.aichat { border: 1px solid rgba(var(--green-rgb), 0.5); }
.ai-model {
background: rgba(var(--green-rgb), 0.16);
padding: 0.35rem 0.8rem !important;
font-weight: 700; letter-spacing: 0.1em; font-size: 0.75rem;
}
.ai-system { padding: 0.4rem 0.8rem; font-size: 0.8rem; opacity: 0.85; }
.ai-system pre { white-space: pre-wrap; margin: 0.4rem 0 0 0; }
.ai-body { padding: 0.6rem 0.8rem; }
.ai-turn { margin-bottom: 0.6rem; }
.ai-role { font-size: 0.68rem; letter-spacing: 0.12em; opacity: 0.6; }
.ai-assistant .ai-text { border-left: 2px solid rgba(46,160,120,0.6); padding-left: 0.6rem; }
/* Email */
.email { border: 1px solid rgba(var(--ink-rgb),0.45); padding: 0.7rem 0.9rem; }
.email-head td { padding: 0 0.6rem 0.1rem 0; vertical-align: top; font-size: 0.82rem; }
.email-head td:first-child { opacity: 0.6; }
.email-body { margin-top: 0.6rem; }
/* Handouts */
.handout {
border: 1px solid currentColor;
padding: 1rem 1.2rem;
font-family: "Courier Prime", "Courier New", monospace;
}
.handout-title {
font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
border-bottom: 1px solid currentColor; padding-bottom: 0.4rem;
margin-bottom: 0.7rem !important;
}
.handout-table { width: 100%; font-size: 0.85rem; margin: 0.5rem 0; }
.handout-table td.k { opacity: 0.65; width: 34%; padding-right: 1rem; vertical-align: top; }
/* --- Prose between scenes ---------------------------------------- */
.prose {
max-width: 64ch;
margin: 1.5rem 0 2rem 0;
font-family: "Iowan Old Style", "Palatino Linotype", Palatino,
"Book Antiqua", Georgia, serif;
font-size: 1.06rem;
line-height: 1.65;
color: var(--ink);
}
.prose p {
margin: 0 0 1.05rem 0;
}
.prose p:first-child { margin-top: 0; }
.prose p:last-child { margin-bottom: 0; }
blockquote.turn {
max-width: 64ch;
border-left: 3px solid rgba(var(--violet-rgb), 0.6);
padding-left: 1rem;
margin: 1.4rem 0 0.6rem 0;
}
.name-confirm {
margin: 0.7rem 0 0 0;
font-weight: 600;
}
/* --- The d20 ------------------------------------------------------ */
.roll {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 60ch;
font-family: ui-monospace, "Courier New", monospace;
}
.roll-head {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
letter-spacing: 0.12em;
opacity: 0.75;
margin-bottom: 0.5rem;
}
.roll-bar { display: flex; gap: 0.7rem; align-items: center; margin-top: 0.3rem; }
.roll-go, .roll-rewrite {
font: inherit;
font-size: 0.82rem;
padding: 0.25rem 0.9rem;
border-radius: 4px;
border: 1px solid rgba(var(--ink-rgb), 0.5);
background: rgba(var(--violet-rgb), 0.15);
cursor: pointer;
}
.roll-rewrite { background: rgba(var(--coral-rgb), 0.12); }
.roll-go:hover { background: rgba(var(--violet-rgb), 0.3); }
.roll-rewrite:hover { background: rgba(var(--coral-rgb), 0.25); }
.roll-value { font-size: 0.9rem; letter-spacing: 0.05em; }
.roll-out {
margin: 0.6rem 0 0 0 !important;
font-family: "Courier Prime", "Courier New", monospace;
min-height: 1.2rem;
}
.roll-echo { font-size: 0.8rem; opacity: 0.8; }
.roll-note { font-size: 0.8rem; opacity: 0.6; margin: 0.4rem 0 0 0 !important; }
.roll-foot {
font-size: 0.72rem;
letter-spacing: 0.1em;
opacity: 0.55;
margin: 0.5rem 0 0 0 !important;
border-top: 1px solid rgba(var(--ink-rgb),0.25);
padding-top: 0.35rem;
}
.roll-cue {
margin: 0.6rem 0 0 4ch !important;
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700;
letter-spacing: 0.06em;
}
.roll-line {
margin: 0 0 0 6ch !important;
max-width: 40ch;
font-family: "Courier Prime", "Courier New", monospace;
}
.roll-out .roll-line:first-child { margin-left: 0 !important; max-width: none; }
.companion-name {
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700;
letter-spacing: 0.14em;
font-size: 1.05rem;
margin: 0.7rem 0 0.3rem 0 !important;
}
.companion-intro { max-width: 58ch; margin: 0 0 0.3rem 0 !important; }
.session { max-width: 60ch; margin: 1.1rem 0; }
.session-who {
font-family: "Courier Prime", "Courier New", monospace;
font-weight: 700; letter-spacing: 0.12em; margin: 0 0 0.3rem 0 !important;
}
.session-text { margin: 0 !important; }
.ac-name { border-bottom: 1px dotted rgba(var(--violet-rgb), 0.55); }
.companion-naming { margin-top: 0.6rem; }
/* --- Plots -------------------------------------------------------- */
.plot {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
}
.plot-controls {
display: flex; gap: 1rem; flex-wrap: wrap;
font-size: 0.8rem; opacity: 0.85; margin: 0.4rem 0;
}
.plot-controls input {
font: inherit; font-size: 0.8rem; width: 7ch;
padding: 0.15rem 0.35rem;
border: 1px solid rgba(var(--ink-rgb),0.45);
border-radius: 3px;
background: rgba(var(--ink-rgb),0.06);
}
.plot-status { font-size: 0.75rem; letter-spacing: 0.1em; opacity: 0.7; }
.plot canvas {
display: block;
margin-top: 0.6rem;
background: var(--paper);
border: 1px solid rgba(var(--ink-rgb),0.3);
border-radius: 3px;
}
.roll-go:disabled, .roll-rewrite:disabled {
opacity: 0.4;
cursor: default;
}
.sketch {
border: 1px solid rgba(var(--violet-rgb), 0.5);
border-radius: 5px;
padding: 0.7rem 0.9rem;
margin: 1.2rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
}
.sketch canvas {
display: block;
margin-top: 0.7rem;
background: var(--paper);
border: 1px solid rgba(var(--ink-rgb),0.3);
border-radius: 3px;
max-width: 100%;
}
.sketch-page {
margin-top: 0.7rem;
padding: 0.5rem 0.6rem;
border: 1px solid rgba(var(--ink-rgb),0.3);
border-radius: 3px;
background: rgba(var(--ink-rgb),0.04);
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.9rem;
}
.sketch-page button {
font: inherit; font-size: 0.85rem;
border: 1px solid rgba(var(--ink-rgb),0.45);
border-radius: 3px;
background: rgba(var(--ink-rgb),0.08);
cursor: pointer;
min-width: 2ch;
}
/* --- The page itself, when Quarto is not doing it for us ---------- */
.screenplay, .artifact, .prose, .handout, .beats, .roll, .plot, .sketch,
.todo, .puzzle-box, .personae, .name-field, .live-cell, .session {
color: var(--ink);
}
.handout, .puzzle-cell, .sign { border-color: var(--rule); }
.terminal { background: var(--panel-hard); color: var(--ink); }
.title-page .title, .title-page .subtitle { color: var(--ink); }
/* --- Form controls -------------------------------------------------
Inputs, textareas and buttons do not inherit colour from the page,
so every one of them needs telling, or the reader types black on
black and cannot see what they are writing.
------------------------------------------------------------------- */
.name-inputs input,
.companion-input,
.todo-input,
.plot-controls input,
.live-src,
.sketch textarea,
.sketch-page input {
color: var(--ink);
background: var(--panel);
border-color: var(--rule);
caret-color: var(--ink);
}
.name-inputs input::placeholder,
.companion-input::placeholder,
.todo-input::placeholder,
.sketch-page input::placeholder,
.live-src::placeholder {
color: var(--ink-soft);
opacity: 1;
}
.roll-go, .roll-rewrite, .live-run, .todo-del,
.sketch-page button, .puzzle-cell {
color: var(--ink);
}
.sketch-page button {
background: var(--panel);
border-color: var(--rule);
}
/* Quarto's own search box does not follow the book's palette on every
theme, so it is told here as well. */
#quarto-search input,
#quarto-search .aa-Input,
.aa-Form,
.aa-InputWrapper input,
.quarto-search input {
background: var(--panel) !important;
color: var(--ink) !important;
border-color: var(--rule) !important;
}
.aa-Form { border: 1px solid var(--rule) !important; }
#quarto-search input::placeholder,
.aa-Input::placeholder {
color: var(--ink-soft) !important;
opacity: 1;
}
.aa-DetachedSearchButton, .quarto-search-button {
background: var(--panel) !important;
color: var(--ink) !important;
border-color: var(--rule) !important;
}
/* Canvases inherit nothing, so the sketch cells read their ink off the
element's own resolved colour. This is the line that gives them one. */
.sketch canvas, .plot canvas {
color: var(--ink);
background: transparent;
}
.tribute-heading {
margin: 2rem 0 0.5rem;
font-variant: small-caps;
font-weight: 600;
letter-spacing: 0.04em;
}
.tribute p {
margin: 0.6rem 0;
}
.tribute-meta {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--bs-border-color);
text-align: right;
font-size: 0.85rem;
}
.tribute-meta p {
margin: 0.25rem 0;
}
.tribute-date {
font-style: italic;
}
.tribute-attribution {
font-size: 1rem;
}
.tribute-source {
margin-top: 0.75rem !important;
}
.footnote {
margin-top: 1.5rem;
font-size: 0.9rem;
color: var(--bs-secondary);
}
/* ---------------------------------------------------------------
The three endings.
All three are in the HTML. Which one is visible is a class on
<body>, written once by fedoras.reader/paint-standing! from the
reader's standing, so a chapter that forks does it here rather than
in nine lines of DOM work per passage.
A page with no class on it shows the middle one. That is what the
print edition gets, and it is what the table actually got, and the
PDF reads straight through with no gap and no note.
--------------------------------------------------------------- */
.ending { display: none; }
.ending-reconciled { display: block; }
body.standing-closed .ending-reconciled { display: none; }
body.standing-outstanding .ending-reconciled { display: none; }
body.standing-closed .ending-closed { display: block; }
body.standing-outstanding .ending-outstanding { display: block; }
@media print {
.ending { display: none !important; }
.ending-reconciled { display: block !important; }
}
/* ---------------------------------------------------------------
The slips.
--------------------------------------------------------------- */
.slip {
margin: 1.2rem 0 1.2rem 4ch !important;
padding-left: 1rem;
max-width: 46ch;
border-left: 2px solid rgba(var(--coral-rgb), 0.55);
font-family: "Courier Prime", "Courier New", monospace;
font-style: italic;
opacity: 0.8;
white-space: pre-wrap;
}
#quarto-margin-sidebar { display: none; }
.scene-code {
border: 1px solid var(--rule);
border-radius: 3px;
padding: 0.6rem 0.8rem;
margin: 0.9rem 0 1.1rem 0;
max-width: 62ch;
font-family: ui-monospace, "Courier New", monospace;
font-size: 0.88rem;
line-height: 1.45;
background: var(--panel);
color: var(--ink);
overflow-x: auto;
}
.scene-code pre { margin: 0; }
.scene-code code { background: none; padding: 0; }
/* Pressing Enroll flashes the name it confirms, the same name or a new
one, so the press can be seen to have done something. */
.name-confirm .pc-name.pc-flash {
animation: pc-flash 1.4s ease-out;
}
@keyframes pc-flash {
0%, 30% { color: var(--green); }
}</style><script src="scittle.js" type="application/javascript"></script><div style="display:none;"><script type="application/x-scittle">(ns fedoras.carry
"What the reader carries between chapters."
(:require [clojure.string :as str]))
(def ^:private book-key
"The registration on the front of the file. It is not a secret and it
is not pretending to be one — it is the number this is keyed to, and
it is the same number a reader has been looking at since August."
3812)
;; ---------------------------------------------------------------------
;; Bytes
;; ---------------------------------------------------------------------
(defn- to-bytes [s] (.encode (js/TextEncoder.) s))
(defn- from-bytes [b] (.decode (js/TextDecoder. "utf-8") b))
(defn- bytes->str [b]
(loop [i 0 acc ""]
(if (>= i (.-length b))
acc
(recur (inc i) (str acc (js/String.fromCharCode (aget b i)))))))
(defn- str->bytes [s]
(let [out (js/Uint8Array. (count s))]
(dotimes [i (count s)] (aset out i (.charCodeAt s i)))
out))
;; ---------------------------------------------------------------------
;; The keystream
;;
;; The same small xorshift the dice and the floors use, so there is one
;; generator in this project and not four.
;; ---------------------------------------------------------------------
(defn- keystream [n seed]
(let [out (js/Uint8Array. n)]
(loop [i 0 x (bit-or 1 (bit-and seed 0x7fffffff))]
(if (>= i n)
out
(let [x (bit-and (bit-xor x (bit-shift-left x 13)) 0x7fffffff)
x (bit-and (bit-xor x (unsigned-bit-shift-right x 17)) 0x7fffffff)
x (bit-and (bit-xor x (bit-shift-left x 5)) 0x7fffffff)]
(aset out i (bit-and x 0xff))
(recur (inc i) x))))))
(defn- mask [b seed]
(let [k (keystream (.-length b) seed)
out (js/Uint8Array. (.-length b))]
(dotimes [i (.-length b)] (aset out i (bit-xor (aget b i) (aget k i))))
out))
;; ---------------------------------------------------------------------
;; Base64, url-safe
;; ---------------------------------------------------------------------
(defn- b64 [b]
(-> (js/btoa (bytes->str b))
(str/replace "+" "-")
(str/replace "/" "_")
(str/replace "=" "")))
(defn- un-b64 [s]
(let [t (-> s (str/replace "-" "+") (str/replace "_" "/"))
pad (case (mod (count t) 4) 2 "==" 3 "=" "")]
(str->bytes (js/atob (str t pad)))))
;; ---------------------------------------------------------------------
;; Rolls, short
;; ---------------------------------------------------------------------
(defn- pack-rolls
"[roll mod rewrites], shortest form. A roll with no modifier and no
rewrites is one number."
[rolls]
(into {}
(for [[id {:keys [roll mod rewrites]}] rolls
:when roll]
[id (cond
(pos? (or rewrites 0)) [roll (or mod 0) rewrites]
(not (zero? (or mod 0))) [roll mod]
:else [roll])])))
(defn- unpack-rolls
"Every shape this has ever emitted: a bare number, the short vector,
and the verbose map that went out while `pack-rolls` was sitting here
unused. Tokens from before this change still read."
[m]
(into {}
(for [[id v] m]
[(clojure.core/name id)
(cond
(number? v) {:roll v :mod 0 :rewrites 0}
(map? v) {:roll (:roll v)
:mod (or (:mod v) 0)
:rewrites (or (:rewrites v) 0)}
:else {:roll (nth v 0)
:mod (nth v 1 0)
:rewrites (nth v 2 0)})])))
;; ---------------------------------------------------------------------
;; The token
;; ---------------------------------------------------------------------
(defn- checksum [b]
(loop [i 0 s 7]
(if (>= i (.-length b))
(mod s 251)
(recur (inc i) (mod (+ (* s 31) (aget b i)) 251)))))
(defn pack
"A map to a token. The keys are one letter each because this goes in
a query string thirty times a page."
[{:keys [name companion echo rolls deeds epoch todo]}]
(try
(let [payload (js/JSON.stringify
(clj->js
(cond-> {}
name (assoc :n name)
companion (assoc :a [(:n companion) (:given companion)])
(pos? (or echo 0)) (assoc :e echo)
(seq rolls) (assoc :r (pack-rolls rolls))
(seq deeds) (assoc :d (mapv clojure.core/name deeds))
;; when the reader last withdrew the whole record.
;; it travels because the alternative is that only
;; the state travels, and then a link from before
;; the withdrawal quietly reinstates it.
(pos? (or epoch 0)) (assoc :g epoch)
(seq todo) (assoc :t [(:seq todo 0) (:items todo)]))))
body (mask (to-bytes payload) book-key)
out (js/Uint8Array. (inc (.-length body)))]
(aset out 0 (checksum body))
(dotimes [i (.-length body)] (aset out (inc i) (aget body i)))
(b64 out))
(catch :default _ nil)))
(defn unpack
"A token back to a map, or nil if anybody has been at it.
An absent field comes back empty rather than missing, which is fine
and is not a licence: the caller merges this forward and knows that an
empty Echo means the token is old, not that the Echo is nought."
[s]
(try
(when-not (str/blank? s)
(let [raw (un-b64 s)
want (aget raw 0)
body (.slice raw 1)]
(when (= want (checksum body))
(let [m (js->clj (js/JSON.parse (from-bytes (mask body book-key)))
:keywordize-keys true)]
{:name (:n m)
:companion (when-let [a (:a m)] {:n (first a) :given (or (second a) "")})
:echo (or (:e m) 0)
:rolls (unpack-rolls (or (:r m) {}))
:deeds (mapv keyword (or (:d m) []))
:epoch (or (:g m) 0)
:todo (when-let [t (:t m)] {:seq (first t) :items (or (second t) [])})}))))
(catch :default _ nil)))
;; ---------------------------------------------------------------------
;; When it does not reconcile
;; ---------------------------------------------------------------------
(defn complain! []
(js/console.warn
"016.09 — entry does not reconcile and has not been carried forward. Retained."))</script><script type="application/x-scittle">(ns fedoras.reader
"Everything the reader owns.
One atom: containing Name, animal, echo, fortune, score, deeds. A widget
reads it and never touches storage directly, and every change goes through
one of the setters, which writes, re-reads, and lets the watches fire."
(:require [clojure.string :as str]
[fedoras.carry :as carry]))
;; ---------------------------------------------------------------------
;; Storage
;; ---------------------------------------------------------------------
(def prefix "fedoras.")
(def carry-param :f)
(def forget-param
"A page that loads with this on it withdraws its own record and stops.
It is a separate field from the token because it must work on a page
that has never seen a token."
:forget)
(def withdrawn-param
"Survives exactly one reload, to say the thing was done. The state it
would otherwise have been recorded in has just been deleted, which is
the one place a confirmation cannot live."
:withdrawn)
(def legacy-params
"The fields the query string used to carry, one per fact. Stripped off
any link that still has them. There was a second copy of this list
called `carried-params` and the two had drifted."
[:pc :pcl :ac :acn :echo :fortune :score])
(defn- read-json [s]
(js->clj (js/JSON.parse s) :keywordize-keys true))
(defn- write-json [v]
(js/JSON.stringify (clj->js v)))
(defn stored [key]
(try
(when-let [s (.getItem js/localStorage (str prefix (name key)))]
(read-json s))
(catch :default _ nil)))
(defn store! [key val]
(try (.setItem js/localStorage (str prefix (name key)) (write-json val))
(catch :default _ nil))
val)
(defn remove!
"Take something out rather than writing an empty one over it, because
a stored nil and a missing key read back the same and only one of them
is what was meant."
[key]
(try (.removeItem js/localStorage (str prefix (clojure.core/name key)))
(catch :default _ nil)))
(defn storage-works?
"Whether anything at all is going to be persisted.
On a file:// path some browsers give every page its own opaque origin,
which means localStorage throws and the whole book forgets the reader
the moment they click anything. Worth knowing rather than guessing."
[]
(try
(.setItem js/localStorage (str prefix "probe") "1")
(let [ok (= "1" (.getItem js/localStorage (str prefix "probe")))]
(.removeItem js/localStorage (str prefix "probe"))
ok)
(catch :default _ false)))
;; ---------------------------------------------------------------------
;; The query string, which is a transport and nothing else
;; ---------------------------------------------------------------------
(defn- current-url [] (js/URL. (.-href js/location)))
(defn param [key]
(try
(let [v (.get (.-searchParams (current-url)) (name key))]
(when-not (str/blank? v) v))
(catch :default _ nil)))
(defn set-params! [m]
(try
(let [u (current-url) sp (.-searchParams u)]
(doseq [[k v] m]
(if (or (nil? v) (= "" v))
(.delete sp (name k))
(.set sp (name k) (str v))))
(.replaceState js/history nil "" (.toString u)))
(catch :default _ nil)))
(defn- int-or [v d]
(let [n (js/parseInt (str v) 10)] (if (js/isNaN n) d n)))
;; ---------------------------------------------------------------------
;; The atom
;; ---------------------------------------------------------------------
(def default-name ["Owen" "Mercer"])
(def state (atom nil))
(def animal-names
"Number to the animal's grammar: what it is called, what more than one
of them is called, what a ledger would file it under. Filled in by
`fedoras.state/assets` from a later script tag, so it is empty for the
moment between this file being read and the roster arriving, and
nothing here may treat an empty roster as a missing roll."
(atom {}))
(defn- as-grammar
"One roster entry, whatever shape it turned up in.
The page used to be handed {1 \"Pigeon\"} and is now handed
{1 {:animal \"Pigeon\" :plural \"pigeons\" ...}}, and a book whose
`names-form` has not caught up with its roster hands over one of each.
A bare string is an animal with no grammar, which is precisely what it
was before, so it becomes one here.
This is normalised at the boundary rather than at each use because the
alternative was a `merge` three functions away receiving a string,
throwing `conj on a map takes map entries`, and taking down every
render watch on the page — a build that is one function out of step
should read a little worse, not stop."
[v]
(cond
(map? v) v
(string? v) {:animal v}
:else {}))
(defn- read-companion []
(let [s (stored :companion)
n (int-or (first s) nil)]
(when (and n (<= 1 n 20))
(let [given (or (nth s 2 nil) "")
g (as-grammar (get @animal-names n))
;; the animal's name is a convenience and the roster may not
;; have landed yet. the number is the fact.
animal (or (:animal g) (nth s 1 nil) "")
named? (not (str/blank? given))]
(merge {:plural "" :taxonomy "" :collective "" :other-names []}
g
{:n n
:animal animal
:given given
:named? named?
:name (cond named? (str/trim given)
(not (str/blank? animal)) animal
:else (str "d20 " n))})))))
(defn- read-name []
(let [s (stored :pc)]
[(or (first s) (first default-name))
(or (second s) (second default-name))]))
(defn refresh!
"Total on purpose. Every render watch in the book hangs off this atom,
so anything that throws in here blanks the whole page, and what it
reads is JSON that a previous version of this file wrote, which is not
a thing to be optimistic about. A key that is the wrong shape reads as
absent."
[]
(reset! state
{:name (read-name)
:companion (try (read-companion) (catch :default _ nil))
:echo (int-or (stored :echo) 0)
:rolls (let [r (stored :rolls)] (if (map? r) r {}))
:deeds (let [d (stored :deeds)]
(set (map keyword (if (sequential? d) d []))))
:epoch (int-or (stored :epoch) 0)
:todo (let [t (stored :todo)] (if (sequential? t) t []))
:todo-seq (int-or (stored :todo-seq) 0)}))
;; ---------------------------------------------------------------------
;; What a chapter may ask for
;; ---------------------------------------------------------------------
(defn reader-name [] (:name @state))
(defn companion [] (:companion @state))
(defn echo [] (:echo @state))
(defn deeds [] (:deeds @state))
(defn fortune [] (count (:deeds @state)))
(defn epoch
"When this reader last withdrew the whole record, in seconds, or
nought if they never have.
It is not a clock and nothing displays it. It exists so that a
withdrawal can be compared against a link: a token stamped earlier
than this was written by somebody who has since been forgotten, and it
is not allowed to put them back."
[]
(:epoch @state))
(defn standing
"Fortune less echo. Added to every roll for the rest of the book, and
the number the last chapter reads to decide how it ends."
[]
(- (fortune) (echo)))
(defn registration [] "3812")
;; ---------------------------------------------------------------------
;; And what it may change
;; ---------------------------------------------------------------------
(defn set-name! [f l]
(store! :pc [(if (str/blank? f) (first default-name) (str/trim f))
(if (str/blank? l) (second default-name) (str/trim l))])
(refresh!))
(defn set-companion!
"Three cells: the number, the animal's own name as a courtesy, and
whatever the reader called it. The number is the only one that has to
be right, because the other two are recovered from the roster."
[n given]
(store! :companion [n (get-in @animal-names [n :animal] "") (or given "")])
(refresh!))
(defn set-echo! [n] (store! :echo (max 0 n)) (refresh!))
(defn add-echo!
"One more thing taken back."
[]
(set-echo! (inc (echo))))
(defn did!
"Record a deed. A set, so doing the same thing twice is doing it once,
and nothing anywhere says that it happened."
[k]
(store! :deeds (mapv name (conj (deeds) (keyword k))))
(refresh!))
(defn animals!
"The roster's grammar, from the page. Normalised on the way in, so the
shape `fedoras.state/names-form` happens to be emitting this week is
something only this function has an opinion about.
Refreshes, because a companion read before this arrived is holding an
empty animal name."
[m]
(reset! animal-names
(if (map? m)
(into {} (map (fn [[k v]] [k (as-grammar v)])) m)
{}))
(refresh!))
(defn rolls
"Every check the reader has resolved, by id.
Normalised on the way out. Storage keywordizes keys on read, and a
check id is an arbitrary string rather than a name, so the keys come
back as keywords and nothing matches them. And anything that is not a
map at all becomes an empty one, because assoc on a vector with a
string key takes the page down."
[]
(let [r (:rolls @state)]
(if (map? r)
(into {} (map (fn [[k v]] [(name k) v])) r)
{})))
(defn roll-entry [id]
(when-let [e (get (rolls) id)]
{:roll (or (:roll e) (get e "roll"))
:mod (or (:mod e) (get e "mod") 0)
:rewrites (or (:rewrites e) (get e "rewrites") 0)}))
(defn remember-roll! [id e]
(store! :rolls (assoc (rolls) id e))
(refresh!))
(defn score []
(+ (reduce + 0 (keep :roll (vals (rolls))))
(or (:n (companion)) 0)))
;; ---------------------------------------------------------------------
;; Import, once, on arrival
;; ---------------------------------------------------------------------
(def carries-epoch?
"Whether the codec on this page round-trips a withdrawal stamp."
(try
(= 12345 (:epoch (carry/unpack
(carry/pack {:name ["Owen" "Mercer"] :epoch 12345}))))
(catch :default _ false)))
(defn- wipe!
"Everything under the prefix, and then one thing: when that happened."
[at]
(try
(let [ks (vec (for [i (range (.-length js/localStorage))
:let [k (.key js/localStorage i)]
:when (and k (str/starts-with? k prefix))]
k))]
(doseq [k ks] (.removeItem js/localStorage k)))
(catch :default _ nil))
(store! :epoch at))
(defn- merge-token!
[{:keys [name companion echo rolls deeds epoch todo]}]
(let [ours (int-or (stored :epoch) 0)
theirs (or epoch 0)]
(cond
;; A link from before the reader withdrew the record. It is a true
;; account of somebody who does not exist any more, which is not
;; the same thing as an instruction to rebuild them. This is the
;; case that made `forget!` a lie: it emptied one page's storage
;; and every other page, and every entry in the back history, was
;; still holding a complete copy and handed it straight back.
;; A link from before the reader withdrew the record. It is a true
;; account of somebody who does not exist any more, which is not
;; the same thing as an instruction to rebuild them.
;;
;; Only when the codec can say so. A page that cannot read a stamp
;; off a token must not conclude from its absence that every link
;; in the book is stale.
(and carries-epoch? (< theirs ours))
(js/console.info
(str "016.09 — the link this page was opened from was written before the record "
"was withdrawn. Nothing carried forward. Reach this chapter from a page you "
"have opened since, and whatever is current will arrive with you."))
:else
(do
;; They withdrew it somewhere this page has not heard about yet.
;; The erasure travels the way the state travels, which is the
;; only way anything travels in a book read off a disk.
(when (and carries-epoch? (> theirs ours))
(wipe! theirs)
(js/console.info
"016.09 — prior record withdrawn at the reader's request. Nothing carried forward."))
;; the name is the one thing a reader can retype in five seconds,
;; so the token is allowed to have an opinion about it
(when (seq name)
(store! :pc name))
;; The animal:
;;
;; This was write-once — take the token's only if there is
;; nothing here — which is right on one origin and wrong on a
;; folder of files, where every page has its own storage and the
;; token is the only thing that crosses. A reader who opened Act
;; II after rolling and before naming taught that page the horse,
;; permanently, and Act II could never afterwards learn that the
;; horse was called Horace.
;;
;; The Echo is the recency stamp. It only ever goes up, and a
;; reroll raises it, so a token carrying an Echo at least as
;; large as this page's is at least as new as this page and is
;; allowed to speak. One older, and it is a link from before the
;; reroll and is not.
(let [have (read-companion)
here-echo (int-or (stored :echo) 0)
there-echo (or echo 0)]
(when (and (:n companion)
(or (nil? have) (>= there-echo here-echo)))
(store! :companion
[(:n companion) ""
;; a blank name in a token is a reader who had not
;; named it yet, not a reader who unnamed it
(if (str/blank? (:given companion))
(or (:given have) "")
(:given companion))])))
;; upward only, always. after the companion, which reads it.
(store! :echo (max (int-or (stored :echo) 0) (or echo 0)))
;; a check is resolved once, and what is already in this browser
;; was rolled by this reader, so it wins every collision
(when (seq rolls)
(let [named (fn [m] (if (map? m)
(into {} (map (fn [[k v]] [(clojure.core/name k) v])) m)
{}))]
(store! :rolls (merge (named rolls) (named (stored :rolls))))))
;; deeds are a set, and a set does not get smaller by being described
(when (seq deeds)
(let [held (let [d (stored :deeds)] (if (sequential? d) d []))]
(store! :deeds (vec (into (set (map clojure.core/name held))
(map clojure.core/name deeds))))))
(when-let [t todo]
(when (>= (:seq t 0) (int-or (stored :todo-seq) 0))
(store! :todo (:items t))
(store! :todo-seq (:seq t))))))))
(defn- import-token! []
(if-let [t (param carry-param)]
(if-let [m (try (fedoras.carry/unpack t) (catch :default _ nil))]
(merge-token! m)
(try (fedoras.carry/complain!) (catch :default _ nil)))
;; nothing to do, unless somebody has a link from before the token
(do
(when-let [pc (param :pc)]
(store! :pc [pc (or (param :pcl) (second default-name))]))
(when-let [ac (param :ac)]
(let [n (int-or ac nil)]
(when (and n (<= 1 n 20) (nil? (read-companion)))
(store! :companion [n "" (or (param :acn) "")]))))
(when-let [e (param :echo)]
(store! :echo (max (int-or (stored :echo) 0) (int-or e 0)))))))
(defn- withdraw-if-asked!
"A page loaded with a withdrawal stamp on it clears itself and does
nothing else.
This is how one button reaches twelve documents that cannot see each
other. The page the reader clicked opens each of the others in a
hidden frame with the stamp in the query string, and each one runs
this on the way up. Nothing is scripted across the boundary — every
file is its own origin and none of them can touch another's storage —
but a document can always clear its own, and a query string is the one
thing that crosses.
Returns true if this load was a withdrawal, so boot knows to skip the
rest of its work."
[]
(when-let [at (some-> (param forget-param) (int-or nil))]
(when (> at (int-or (stored :epoch) 0))
(wipe! at))
true))
;; ---------------------------------------------------------------------
;; Rendering
;; ---------------------------------------------------------------------
(defn on-render! [k f] (add-watch state k (fn [_ _ _ _] (f))) (f))
(defn el [id] (.getElementById js/document id))
(defn q [node selector] (when node (.querySelector node selector)))
(defn q-all
"Every match, as a vector. One argument searches the document; two
searches inside a node.
Written the long way because a NodeList is not a Clojure collection
and array-seq is not in Scittle's bindings."
([selector] (q-all js/document selector))
([node selector]
(if-not node
[]
(let [nodes (.querySelectorAll node selector)]
(mapv (fn [i] (.item nodes i)) (range (.-length nodes)))))))
(defn text! [node selector s]
(when-let [t (q node selector)] (set! (.-textContent t) s)))
(defn on-click! [node selector f]
(when-let [b (q node selector)] (set! (.-onclick b) f)))
(defn enable! [node selector on? label]
(when-let [b (q node selector)]
(set! (.-disabled b) (not on?))
(when label (set! (.-textContent b) label))))
(defn show! [node selector on?]
(when-let [t (q node selector)]
(set! (.-display (.-style t)) (if on? "flex" "none"))))
(defn widget [k id f]
(on-render! k (fn [] (when-let [node (el id)] (f node)))))
;; ---------------------------------------------------------------------
;; Withdrawing the record, everywhere
;;
;; Twelve documents, each its own origin, none able to read another's
;; storage. Every shared channel is shut: localStorage and IndexedDB are
;; origin-scoped, window.name is cleared on the way across, cookies do
;; not apply to a file, and an iframe gets its own origin so the parent
;; cannot script it.
;;
;; It can still LOAD it. A hidden frame pointed at a sibling chapter,
;; with a stamp in the query string, runs that chapter's own copy of
;; this file, which clears that chapter's own storage. Nothing crosses
;; the boundary except the URL, which is the one thing that was ever
;; going to.
;; ---------------------------------------------------------------------
(defn- sibling-pages
"Every other chapter this page links to.
Read off the sidebar rather than configured, because Quarto already
puts the whole book in it and a hand-kept list of filenames is a list
that goes stale the first time a chapter is renamed."
[]
(let [here (.-pathname js/location)
dir (subs here 0 (inc (.lastIndexOf here "/")))]
(->> (q-all "a[href]")
(keep (fn [a]
(let [h (.getAttribute a "href")]
(when (and h
(not (str/starts-with? h "#"))
(not (re-find #"(?i)^(https?:|mailto:|javascript:)" h)))
(try
(let [u (js/URL. (.-href a))
p (.-pathname u)]
(when (and (str/starts-with? p dir)
(str/ends-with? (str/lower-case p) ".html")
(not= p here))
(str (.-protocol u) "//" (.-host u) p)))
(catch :default _ nil))))))
distinct
vec)))
(defn- sweep!
"Tell every other chapter to withdraw, then call `done` once.
Once, whatever happens: a frame that never fires must not leave the
reader looking at a button that has stopped meaning anything, so there
is a deadline as well as a count."
[at done]
(let [pages (sibling-pages)
fired (atom false)
finish (fn [] (when-not @fired (reset! fired true) (done)))]
(if (empty? pages)
(finish)
(let [box (.createElement js/document "div")
left (atom (count pages))
one (fn [] (when (zero? (swap! left dec)) (finish)))]
(js/console.info "016.09 — withdrawing from" (count pages) "other chapters")
(set! (.-style box)
"position:absolute;width:0;height:0;overflow:hidden;border:0;visibility:hidden")
(.appendChild (.-body js/document) box)
(doseq [p pages]
(let [f (.createElement js/document "iframe")]
(set! (.-onload f) (fn [_] (one)))
(set! (.-onerror f) (fn [_] (one)))
(set! (.-src f) (str p "?" (clojure.core/name forget-param) "=" at))
(.appendChild box f)))
(js/setTimeout finish 5000)))))
(defn forget!
"Forget everything, not the six things somebody remembered to list,
and not only on this page.
Emptying this document's storage was never the whole job: the reader
was also sitting, complete, in the query string of every link on every
other chapter and in every entry of the back history, and the next
navigation put them back. So this clears here, stamps when, and then
visits every other chapter to say so. The stamp is what makes a link
written before the withdrawal refuse to be believed afterwards.
It also says so while it is happening. It used to do all of this in
silence and then reload, which is indistinguishable from a button that
does nothing except reload."
[]
(let [at (quot (.now js/Date) 1000)
btn (q js/document ".pc-forget")]
(when btn
(set! (.-disabled btn) true)
(set! (.-textContent btn) "Withdrawing the record…"))
(wipe! at)
(sweep! at
(fn []
(let [u (current-url)]
(doseq [k (concat [carry-param forget-param] legacy-params)]
(.delete (.-searchParams u) (clojure.core/name k)))
(.set (.-searchParams u) (clojure.core/name withdrawn-param) "1")
(set! (.-href js/location) (.toString u)))))))
(defn- note-withdrawal!
"One line, on the way back up, and then the button goes back to
offering. The confirmation cannot be stored, because storing things is
what has just been undone — so it is read off the query string once,
before anything else in boot is allowed to tidy the query string away."
[withdrew?]
(when withdrew?
(js/console.info
"016.09 — record withdrawn from this chapter and from every chapter linked from it.")
(when-let [b (q js/document ".pc-forget")]
(set! (.-textContent b) "Record withdrawn")
(js/setTimeout (fn [] (set! (.-textContent b) "Forget everything")) 5000))))
;; ---------------------------------------------------------------------
;; Export
;; ---------------------------------------------------------------------
(defn token
"What this reader looks like from outside. Recomputed rather than
stored, so it cannot disagree with the state it is describing.
Guarded, because this runs on every render and inside `decorate-links!`,
and a codec that throws on one unexpected key should cost the reader a
shareable link and nothing else."
[]
(try
(let [{:keys [name companion echo rolls deeds todo todo-seq]} @state]
(fedoras.carry/pack
{:name name
:companion (when companion (select-keys companion [:n :given]))
:echo echo
:rolls rolls
:deeds deeds
:epoch (epoch)
:todo (when (seq todo) {:seq (or todo-seq 0) :items todo})}))
(catch :default e
(js/console.warn "fedoras: could not pack the carry token —" e)
nil)))
(defn decorate-links!
"Every internal link carries one field.
Two things were wrong before this and both of them mattered. It ran
inline, at parse time, so any link below the script tag did not exist
yet — which is every prev-and-next arrow Quarto puts in the footer.
And it copied the query string rather than the state, so a page opened
from storage handed on nothing."
[]
(when-let [t (token)]
(doseq [a (q-all "a[href]")]
(let [h (.getAttribute a "href")]
(when (and h
(not (str/starts-with? h "#"))
(not (re-find #"(?i)^(https?:|mailto:|javascript:)" h)))
(try
(let [lu (js/URL. (.-href a))
sp (.-searchParams lu)]
(doseq [k legacy-params] (.delete sp (clojure.core/name k)))
(.set sp (clojure.core/name carry-param) t)
(set! (.-href a) (.toString lu)))
(catch :default _ nil)))))))
(defn- rewrite-address!
"Put the token in this page's own address bar and take the old fields
off it, so a reader who arrived on a legacy link stops looking at
their own Echo."
[]
(when-let [t (token)]
(set-params! (into {(clojure.core/name carry-param) t}
(map (fn [k] [k nil])
(concat [forget-param withdrawn-param] legacy-params))))))
;; ---------------------------------------------------------------------
;; Tokens
;;
;; It used to be a chain of str/replace calls, which is why {pc-last}
;; worked in a scene and rendered as the literal characters {pc-last}
;; inside a dice passage.
;;
;; Case follows the token: {ac} is `ted`, {Ac} is `Ted`, {AC} is `TED`,
;; so a passage can start a sentence or write a cue line without the
;; author reaching for a second token. Note that this makes {PC} in an
;; already-written scene render as OWEN rather than Owen; the old regex
;; was case-insensitive and threw the information away.
;;
;; An unkown token is passed over: A brace in a passage that is not
;; something this understands comes out exactly as it went in, so this
;; is safe to run over arbitrary prose.
;; ---------------------------------------------------------------------
(def ^:private token-re #"\{([A-Za-z]+)(-[A-Za-z-]+)?\}")
(defn- apply-case [head s]
(cond
(str/blank? s) s
(= head (str/upper-case head)) (str/upper-case s)
(= (subs head 0 1) (str/upper-case (subs head 0 1)))
(str (str/upper-case (subs s 0 1)) (subs s 1))
:else s))
(defn render
"Replace every {token} the resolver knows. `resolve` is called with the
lower-cased head and the lower-cased tail — \"ac\" and \"-plural\" for
{ac-plural} — and returns a string or nil."
[resolve s]
(str/replace (or s "") token-re
(fn [m]
(let [[whole head tail] (if (string? m) [m m nil] m)]
(or (some->> (resolve (str/lower-case head)
(some-> tail str/lower-case))
(apply-case head))
whole)))))
(defn- string-hash
"Stable across loads, which is the point: the same sentence picks the
same alternative name every time the reader opens the page, so nothing
reads as though it were rolled twice."
[s]
(reduce (fn [a i] (mod (+ (* 31 a) (.charCodeAt s i)) 1000003))
7 (range (count s))))
(defn- an [s]
(str (if (re-find #"(?i)^[aeiou]" (str s)) "an " "a ") s))
(defn- animal-tokens
"What a passage may say about the animal. The reader's own name for it
is a name; everything else is the species, because a reader who called
a marmot Ted did not thereby decide that two of them are Teds."
[source tail]
(when-let [{:keys [animal plural taxonomy collective other-names name named?]}
(companion)]
(let [kind (str/lower-case (or animal ""))]
(case (or tail "-name")
"-name" (if named? name (str "the " kind))
"-kind" kind
"-a" (an kind)
"-plural" (if (str/blank? plural) (str kind "s") (str/lower-case plural))
"-many" (if (str/blank? collective)
(str "several " (if (str/blank? plural) (str kind "s") plural))
(str (an collective) " of "
(if (str/blank? plural) (str kind "s") plural)))
"-taxonomy" (str/lower-case (or taxonomy kind))
"-given" (if named? name "")
"-other" (if (seq other-names)
(nth other-names (mod (string-hash source) (count other-names)))
(str "the " kind))
nil))))
(defn- reader-tokens [source]
(let [[f l] (reader-name)]
(fn [head tail]
(case head
"pc" (case (or tail "-first")
"-first" f
"-full" (str f " " l)
"-last" l
nil)
"ac" (or (animal-tokens source tail)
;; no roll yet, and a passage still has to read
(case (or tail "-name")
("-name" "-kind" "-other") "the animal"
"-a" "an animal"
"-plural" "animals"
"-many" "several animals"
"-taxonomy" "animal"
"-given" ""
nil))
nil))))
(defn fill-with
"Put the reader into a passage, plus whatever else the caller knows.
The dice box passes the die in this way."
[extra s]
(let [own (reader-tokens s)]
(render (fn [h t] (or (extra h t) (own h t))) s)))
(defn fill
"Put the reader into a passage. The only place any of these are
resolved."
[s]
(fill-with (fn [_ _] nil) s))
(defn animal-label
"Kept for anything still asking. The display name, as stored, not
shouted — the old one upper-cased and every caller lower-cased it
again somewhere else."
[]
(when-let [c (companion)] (:name c)))
;; ---------------------------------------------------------------------
;; The carry-over
;;
;; What a reader takes to the next book.
;; ---------------------------------------------------------------------
(def on-the-record
"What goes in the token. Everything else is furniture.
A key is on this list because it is small and because losing it would
lose the reader's year. Adding one is a decision about how long every
link on every page is going to be, so it is a list and not a
convention."
#{:pc :companion :echo :rolls :deeds :todo})
(defn kept
"Read something the reader left in a room. No widget that calls this
needs to know anything about tokens."
[k]
(stored k))
(defn keep!
"Leave something in a room. It stays where it was made."
[k v]
(store! k v)
(when (contains? on-the-record k) (refresh!))
v)
(def ^:private alphabet "23456789ABCDEFGHJKLMNPQRSTUVWXYZ")
(defn- b32 [n len]
(loop [n n i 0 out ""]
(if (= i len)
out
(recur (quot n 32) (inc i)
(str (nth alphabet (mod n 32)) out)))))
;; ---------------------------------------------------------------------
;; The name and the animal, on every page
;; ---------------------------------------------------------------------
(defn- paint-name []
(let [[f l] (reader-name)]
(doseq [node (q-all ".pc-name")]
(set! (.-textContent node)
(apply-case (or (.getAttribute node "data-case") "pc")
(case (.getAttribute node "data-pc")
"-full" (str f " " l) "-last" l f))))
(when-let [i (el "pc-first")] (when (str/blank? (.-value i)) (set! (.-value i) f)))
(when-let [i (el "pc-last")] (when (str/blank? (.-value i)) (set! (.-value i) l)))))
(defn- paint-companion
"Each span carries the token that made it, so {ac-plural} written in a
scene at build time still says `marmots` after the reader rolls a
marmot. It used to carry nothing and every span got the same word."
[]
(doseq [node (q-all ".ac-name")]
(let [tail (or (.getAttribute node "data-ac") "-name")
head (or (.getAttribute node "data-case") "ac")]
(set! (.-textContent node)
(or (some->> ((reader-tokens (or (.-textContent node) "")) "ac" tail)
(apply-case head))
"the animal")))))
(defn- paint-tokens []
(doseq [node (q-all ".tok")]
(set! (.-textContent node) (fill (.getAttribute node "data-tok")))))
;; ---------------------------------------------------------------------
;; Ending / Standing
;; ---------------------------------------------------------------------
(def deed-roll
[:nerve ;; a natural twenty with the Echo at nought
:named-it ;; the animal was given a name and kept it
:kept-the-programmes ;; both sheets went in the bag and stayed there
:no-name-for-the-box ;; nothing was spoken into the undertaker's box
:read-it-back ;; the ledger was read against the room out loud
:opened-the-cage
:held-the-door
:went-back-for-it
:signed-nothing
:walked-out
:told-a-new-one ;; Ariathne was paid in something that was theirs
:said-why]) ;; the chancellor asked, and got a true answer
(def ^:private deed-bits 15)
(defn- deed-mask []
(let [held (deeds)]
(reduce (fn [acc [index deed]]
(if (contains? held deed)
(+ acc (bit-shift-left 1 index))
acc))
0
(map-indexed vector deed-roll))))
(defn- deeds-from-mask [mask]
(set (keep-indexed (fn [index deed]
(when (pos? (bit-and mask (bit-shift-left 1 index)))
deed))
deed-roll)))
(def band-code {:closed 0 :reconciled 1 :outstanding 2})
(def code-band {0 :closed 1 :reconciled 2 :outstanding})
(defn ending-band []
(let [reader-standing (standing)]
(cond
(>= reader-standing 4) :outstanding
(>= reader-standing 0) :reconciled
:else :closed)))
(defn- paint-standing!
"One class on the body, so a chapter that forks three ways does it in
the stylesheet instead of in nine lines of DOM work per passage. A page
with no class on it shows :reconciled, which is what the print edition
gets and what the table actually got.
This existed and was never wired to anything, which meant every
`sp/by-standing` on every page was rendering all three endings at
once, or none."
[]
(when-let [body (.-body js/document)]
(let [classes (.-classList body)]
(doseq [old ["standing-closed" "standing-reconciled" "standing-outstanding"]]
(.remove classes old))
(.add classes (str "standing-" (clojure.core/name (ending-band)))))))
(def code-version 3)
(defn- check-char [body]
(nth alphabet
(mod (reduce + (map (fn [c] (.indexOf alphabet c)) body)) 32)))
(defn carry-code
"What this reader looks like to the next book, as an accession line."
[]
(let [n (+ (* code-version 4294967296)
(* (get band-code (ending-band) 1) 1073741824)
(* (min 31 (or (:n (companion)) 0)) 33554432)
(* (min 31 (echo)) 1048576)
(* (deed-mask) 32)
(min 31 (quot (score) 10)))
body (b32 n 7)]
(str "016.14 · " (registration) " · "
(subs body 0 4) "-" (subs body 4) (check-char body))))
(defn read-carry
"The other side of it, for the book after this one."
[s]
(let [cleaned (-> (str s) str/upper-case (str/replace #"[^2-9A-Z]" ""))
tail (subs cleaned (max 0 (- (count cleaned) 8)))]
(when (= 8 (count tail))
(let [body (subs tail 0 7)
n (reduce (fn [acc c] (+ (* 32 acc) (.indexOf alphabet c))) 0 body)]
(when (= (nth tail 7) (check-char body))
{:version (quot n 4294967296)
:band (get code-band (mod (quot n 1073741824) 4))
:companion (mod (quot n 33554432) 32)
:echo (mod (quot n 1048576) 32)
:deeds (deeds-from-mask (mod (quot n 32) (bit-shift-left 1 deed-bits)))
:score-floor (* 10 (mod n 32))})))))
;; ---------------------------------------------------------------------
;; Is any of this working
;;
;; Type fedoras.reader.diagnose_BANG_() in the console. Faster than
;; opening the storage inspector and reading JSON, and it says whether
;; the token agrees with the storage, which is the failure that cost an
;; animal.
;; ---------------------------------------------------------------------
(defn diagnose! []
(let [t (param carry-param)]
(js/console.log "storage works: " (storage-works?))
(js/console.log "origin: " (.-origin js/location)
(if (= "file:" (.-protocol js/location))
"— every page may have its own storage"
"— one bucket for the whole book"))
(js/console.log "name: " (pr-str (reader-name)))
(js/console.log "companion: " (pr-str (companion)))
(js/console.log " raw storage: " (pr-str (stored :companion)))
(js/console.log "roster loaded: " (count @animal-names) "animals")
(js/console.log "echo / fortune: " (echo) "/" (fortune) " → standing" (standing))
(js/console.log "withdrawn at: " (if (pos? (epoch)) (epoch) "never")
(if carries-epoch?
""
"— CODEC DROPS THE STAMP, carry.cljs is out of date"))
(js/console.log "rolls: " (pr-str (rolls)))
(js/console.log "token on url: " (or t "none"))
(when t
(js/console.log " unpacks to: "
(pr-str (try (fedoras.carry/unpack t)
(catch :default e (str "THREW: " e))))))
(js/console.log "token we'd emit:" (or (token) "none — pack failed"))
:ok))
;; A handle on `window`, because Scittle keeps its namespaces inside the
;; SCI context and the browser console cannot see into it. There is no
;; `fedoras.reader.diagnose_BANG_()` to call — that is the shape a
;; compiled build would have, and this is an interpreted one.
;;
;; fedoras.diagnose()
;; fedoras.forget()
;;
;; and for anything else, scittle.core.eval_string("(fedoras.reader/...)")
(set! (.-fedoras js/window)
(js-obj "diagnose" (fn [] (diagnose!))
"state" (fn [] (clj->js @state))
"token" (fn [] (token))
"forget" (fn [] (forget!))))
;; ---------------------------------------------------------------------
;; Boot
;; ---------------------------------------------------------------------
(if (withdraw-if-asked!)
;; a hidden frame, doing one job. no token, no links, no painting.
(refresh!)
;; read before anything registers a watch: `on-render!` calls its
;; function once on the spot, and the links watch tidies the query
;; string, so the flag is gone by the time the DOM is ready
(let [withdrew? (boolean (param withdrawn-param))]
(when-not carries-epoch?
(js/console.warn
(str "fedoras: carry.cljs does not round-trip the withdrawal stamp. "
"`forget!` will clear this chapter and every chapter linked from it, "
"but a stale link will not be refused afterwards. "
"Update resources/fedoras/carry.cljs — `pack` needs :epoch in its "
"destructuring and :g in its payload, and `unpack` needs :epoch back out.")))
(import-token!)
(refresh!)
(on-render! ::name paint-name)
(on-render! ::companion paint-companion)
(on-render! ::tokens paint-tokens)
(on-render! ::standing paint-standing!)
(on-render! ::links (fn [] (decorate-links!) (rewrite-address!)))
(if (= "loading" (.-readyState js/document))
(.addEventListener js/document "DOMContentLoaded"
(fn [_]
(note-withdrawal! withdrew?)
(decorate-links!)
(paint-standing!)))
(do (note-withdrawal! withdrew?)
(decorate-links!)))))</script><script type="application/x-scittle">(fedoras.reader/animals! {1 {:animal "Pigeon", :plural "pigeons", :taxonomy "bird", :collective "kit", :other-names ["dusty wings" "coooo"]}, 2 {:animal "Giant Centipede", :plural "centipedes", :taxonomy "arthropod", :collective "colony", :other-names ["clicker" "legs gooo"]}, 3 {:animal "Marmot", :plural "marmots", :taxonomy "rodent", :collective "madness", :other-names ["the alarm" "the fat rat"]}, 4 {:animal "Cat", :plural "cats", :taxonomy "felid", :collective "clowder", :other-names ["the cat" "cat"]}, 5 {:animal "Turkey", :plural "turkeys", :taxonomy "fowl", :collective "rafter", :other-names ["thanksgiving dinner for non vegans" "gobbler"]}, 6 {:animal "Giant Badger", :plural "badgers", :taxonomy "mustelid", :collective "cete", :other-names ["angry grandpa" "stripes"]}, 7 {:animal "Giant Weasel", :plural "weasels", :taxonomy "mustelid", :collective "sneak", :other-names ["the scout" "slenderman" "the ermine"]}, 8 {:animal "Fox", :plural "foxes", :taxonomy "canid", :collective "skulk", :other-names ["foxxxy" "spry foot"]}, 9 {:animal "Wolverine", :plural "wolverines", :taxonomy "mustelid", :collective "gang", :other-names ["the small trouble"]}, 10 {:animal "Dog", :plural "dogs", :taxonomy "canid", :collective "pack", :other-names ["the dog" "the ordinary dog"]}, 11 {:animal "Python", :plural "pythons", :taxonomy "serpent", :collective "knot", :other-names ["ssSSssSsss" "coilssss"]}, 12 {:animal "Mule", :plural "mules", :taxonomy "equid", :collective "barren", :other-names ["dapple" "HeeHaw :("]}, 13 {:animal "Lynx", :plural "lynxes", :taxonomy "felid", :collective "clowder", :other-names ["tufts" "yelper"]}, 14 {:animal "Ape", :plural "apes", :taxonomy "primate", :collective "shrewdness", :other-names ["treecrawler"]}, 15 {:animal "Horse", :plural "horses", :taxonomy "equid", :collective "harras", :other-names ["rocinante" "the one nobody rides"]}, 16 {:animal "Moose", :plural "moose", :taxonomy "cervid", :collective "herd", :other-names ["its a giant moose"]}, 17 {:animal "Wyrmling", :plural "wyrmlings", :taxonomy "draconic", :collective "clutch", :other-names ["the wyrmling" "scales"]}, 18 {:animal "Falcon", :plural "falcons", :taxonomy "raptor", :collective "cast", :other-names ["the raptor" "true sight"]}, 19 {:animal "Wolf", :plural "wolves", :taxonomy "canid", :collective "pack", :other-names ["the wolf" "the packleader"]}, 20 {:animal "Bear", :plural "bears", :taxonomy "ursid", :collective "sloth", :other-names ["the bear" "the citadel"]}})</script></div></div>
```
```{=html}
<script type="application/x-scittle">(ns fedoras.ui.dice-ui
"A d20 the reader can roll once.
ROLL ONCE. It used to let you press the button all afternoon. The
handler had no guard and the box painted nothing from storage, so a
roll was neither kept nor refused. It is kept now, the button is spent
once it has been pressed, and the only other door is the expensive
one.
STANDING IS THE MODIFIER. Fortune less echo, added to every roll. It
is stored with the roll rather than read live, because raising the
Echo in Act IV must not quietly reword a scene the reader finished in
Act II.
THE PASSAGE MAY SAY THE NUMBER. Every outcome string is rendered with
the die in scope, so a line can say {result} and mean the number that
was actually rolled rather than the number the author guessed. This is
the reason the passages used to lie: a success wrote `rolls a
fourteen` whatever the reader rolled, and a reader who rolled
seventeen was told, in their own book, that they had not.
A passage is still allowed to be vague. `rolls low` is a choice now
rather than the only option.
SCORE. Every die adds its face to the score, including the ones rolled
again. That is the temptation: a rewrite raises the score and raises
the Echo, and only one of those two is fatal.
FORTUNE. A natural twenty is worth a deed, but only while the Echo is
nought. So the reader who has never taken anything back is the only
one who can still be lucky, and nothing anywhere says so."
(:require [clojure.string :as str]))
;; ---------------------------------------------------------------------
;; What a roll is worth
;; ---------------------------------------------------------------------
(defn band
"One and twenty are themselves. Everything between them is a sum."
[natural dc modifier]
(cond
(= natural 1) :fumble
(= natural 20) :crit
(>= (+ natural modifier) dc) :success
:else :fail))
(defn line
"What was rolled, what it came to, and what that was."
[natural dc modifier]
(str "d20 → " natural
(cond
(pos? modifier) (str " fortune +" modifier " → " (+ natural modifier))
(neg? modifier) (str " echo " modifier " → " (+ natural modifier))
:else "")
" "
(case (band natural dc modifier)
:fumble "FUMBLE" :crit "CRIT" :success "success" :fail "failure")))
;; ---------------------------------------------------------------------
;; The number, in words and in figures
;; ---------------------------------------------------------------------
(def ^:private spelled
["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"
"ten" "eleven" "twelve" "thirteen" "fourteen" "fifteen" "sixteen"
"seventeen" "eighteen" "nineteen" "twenty"])
(defn- in-words [n]
(if (and (number? n) (<= 0 n 20)) (nth spelled n) (str n)))
(defn- magnitude [n] (if (neg? n) (- n) n))
(defn- signed [n]
(cond (pos? n) (str "+" n)
(neg? n) (str "−" (magnitude n))
:else ""))
(defn- arithmetic
"17 + 1 = 18, and just 17 when there is nothing to add, so a line that
uses this reads correctly for a reader whose standing is nought — most
readers, most of the time."
[natural modifier]
(if (zero? modifier)
(str natural)
(str natural " " (if (pos? modifier) "+" "−") " " (magnitude modifier)
" = " (+ natural modifier))))
(defn roll-tokens
"What a passage is allowed to say about the die that produced it.
{result} is the face, because that is what somebody at a table says
out loud. {total} is the number the DC was measured against, which is
a different fact and usually the less interesting one."
[dc {:keys [roll mod]}]
(let [total (+ roll mod)]
(fn [head tail]
(case head
"result" (case (or tail "")
"" (str roll)
"-word" (in-words roll)
"-mod" (arithmetic roll mod)
nil)
"total" (case (or tail "")
"" (str total)
"-word" (in-words total)
nil)
"mod" (when (nil? tail) (signed mod))
"dc" (when (nil? tail) (str dc))
nil))))
;; ---------------------------------------------------------------------
;; Remembering
;; ---------------------------------------------------------------------
(defn- entry [id] (fedoras.reader/roll-entry id))
;; ---------------------------------------------------------------------
;; The passage
;; ---------------------------------------------------------------------
(defn- segment-node [tokens seg]
(let [cue? (str/starts-with? seg "@")
node (.createElement js/document "p")]
(set! (.-className node) (if cue? "roll-cue" "roll-line"))
(set! (.-textContent node)
(fedoras.reader/fill-with tokens (if cue? (subs seg 1) seg)))
node))
(defn- paint-passage! [node tokens text]
(let [box (fedoras.reader/q node ".roll-out")]
(set! (.-innerHTML box) "")
(doseq [seg (str/split (or text "") #"\|")]
(.appendChild box (segment-node tokens seg)))))
;; ---------------------------------------------------------------------
;; The two states a box can be in
;; ---------------------------------------------------------------------
(defn- clear! [node]
(fedoras.reader/text! node ".roll-value" "")
(set! (.-innerHTML (fedoras.reader/q node ".roll-out")) "")
(fedoras.reader/text! node ".roll-note" "")
(fedoras.reader/text! node ".roll-echo" "")
(fedoras.reader/enable! node ".roll-go" true "Roll d20")
(fedoras.reader/show! node ".roll-rewrite" false))
(defn- fill! [node {:keys [dc outcomes]} {:keys [roll mod rewrites] :as e}]
(let [b (band roll dc mod)]
(fedoras.reader/text! node ".roll-value" (line roll dc mod))
(paint-passage! node (roll-tokens dc e)
(or (get outcomes b) (get outcomes :fail)))
(fedoras.reader/text! node ".roll-note"
(if (zero? rewrites)
""
(str "This is version " (inc rewrites) " of what happened.")))
(fedoras.reader/enable! node ".roll-go" false "Rolled")
(fedoras.reader/show! node ".roll-rewrite" true)))
(defn- paint! [node check]
(if-let [e (entry (:id check))]
(fill! node check e)
(clear! node))
(fedoras.reader/text! node ".echo-count" (str (fedoras.reader/echo)))
(fedoras.reader/text! node ".score-count" (str (fedoras.reader/score))))
;; ---------------------------------------------------------------------
;; The two things a reader can do
;; ---------------------------------------------------------------------
(defn- resolve!
"One die, and what it costs and is worth.
The score is not touched here. It is the sum of what is standing, and
what is standing is what this line just wrote."
[node check rewrites]
(let [natural (inc (rand-int 20))
modifier (fedoras.reader/standing)]
;; a twenty is worth a deed, but only to somebody who has taken
;; nothing back. deeds are a set, so it is worth one, once, ever.
(when (and (= 20 natural) (zero? (fedoras.reader/echo)))
(fedoras.reader/did! :nerve))
(fedoras.reader/remember-roll!
(:id check) {:roll natural :mod modifier :rewrites rewrites})
(paint! node check)))
(defn- roll-once! [node check]
(when-not (entry (:id check))
(resolve! node check 0)))
(defn- rewrite!
"Raise the Echo first, then roll under it, so the new number is
penalised by the cost of having asked for it."
[node check]
(when-let [e (entry (:id check))]
(fedoras.reader/add-echo!)
(resolve! node check (inc (:rewrites e)))
(fedoras.reader/text! node ".roll-echo" "Something remembers.")))
;; ---------------------------------------------------------------------
(defn mount! [check]
(fedoras.reader/widget
(keyword "roll" (:id check))
(str "roll-" (:id check))
(fn [node]
(paint! node check)
(fedoras.reader/on-click! node ".roll-go" (fn [_] (roll-once! node check)))
(fedoras.reader/on-click! node ".roll-rewrite" (fn [_] (rewrite! node check))))))</script>
```
# Scene 5 — The Game
```{=html}
<span style="display:none;"></span>
```
```{=html}
<blockquote class="handbook"><p class="rule-number">The Fedora Handbook, Rule #17</p><p class="rule-text">“It's a smart idea to always be sociable. You don't even have to be sociable, just be sociable!”</p></blockquote>
```
```{=html}
<div class="scene"><div class="screenplay"><p class="slug">INT. HALLORAN UNION — ROOM 3C — WEDNESDAY, 7:00 P.M.</p></div><div class="screenplay"><p class="action">The folding table. Five chairs. A tin of dice that used to hold something else.</p><p class="action">VICTOR sits behind a screen turned away from everybody. He is broad and soft and takes up a chair and a half. The ponytail is long, black and wet-looking at the root. The beard has come in on the neck and nowhere else and he keeps it. There is a shine on him under the strip light, at the hairline and along the sides of the nose. The T-shirt has a history and the room is downwind of it.</p><p class="action">On his head is a tall hat of green felt glued over card, which he made himself. It is enormous. It juts. It is for no purpose except being seen from the other end of a room, and it leans, and when he turns his head it arrives a moment later.</p><p class="action beat">Nobody has said anything about the hat since <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> had arrived,
and he wasn't about to be the first.</p><p class="action">MILES has three sheets, two pencils and a plague doctor's mask, which he is wearing. The sheets are his by right. He is the documentarian, and the documentarian is the only person at the table apart from the DM who is permitted to write anything down: what was found, what was said, what was agreed and by whom, and whether it was approved. Anything the documentarian has not recorded did not happen, and Victor rules on this the way other people breathe.</p><p class="action">DEREK and SIMON are not allowed to keep notes and are visibly relieved about it. DEREK has a mug he brought from somewhere and a dice set in a felt bag. SIMON has a dice set lined up in front of him by size and is the only one sitting up straight.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> has paper in front of him tonight only because they are still setting him up, and Miles has looked at it twice already.</p><p class="action">The lid of a pizza box has a grid drawn on it in green marker, twelve by twelve, and four counters on the grid that are bottle caps.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> has a sheet with RANGER at the top in Victor's handwriting and nine numbers on it that somebody else filled in for him. Near the bottom there is a line marked COMPANION and something written on it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">GUILDMASTER [VICTOR]</p><div class="lines"><p class="line">So. The bursar's office has a line in the ledger</p><p class="line">they can't account for. Every month for three</p><p class="line">years the Collegium has paid a salary to a</p><p class="line">lecturer in the natural philosophy faculty, and</p><p class="line">every month it has cleared, and last week somebody</p><p class="line">in accounts finally asked which lecturer.</p></div></div><div class="dialogue"><p class="character">CLERIC [DEREK]</p><div class="lines"><p class="line">And nobody knows who it is.</p></div></div><div class="dialogue"><p class="character">GUILDMASTER [VICTOR]</p><div class="lines"><p class="line">Nobody knows. There's a classroom with his number</p><p class="line">on the door, and a syllabus, and thirty students</p><p class="line">who turn up to it, and not one of them can tell</p><p class="line">you the man's name or what he looks like or the</p><p class="line">last time he stood in front of them.</p></div></div><div class="dialogue"><p class="character">DOCUMENTARIAN [MILES]</p><p class="paren">(muffled, through the mask)</p><div class="lines"><p class="line">Mmf mmff mm mmfff.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody responds.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Mask, Miles.</p></div></div></div><div class="screenplay"><p class="action">Miles pushes the mask up.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">I said if he isn't in the records then he isn't a</p><p class="line">lecturer and the payments are the crime, not the</p><p class="line">man.</p></div></div><div class="dialogue"><p class="character">GUILDMASTER [VICTOR]</p><div class="lines"><p class="line">The payments are why you were sent. Nobody has</p><p class="line">sent you to find a person. They've sent you to</p><p class="line">close a line in a ledger.</p></div></div></div><div class="screenplay"><p class="action">Miles writes that down. It takes a while, because it goes in the ledger and the ledger has columns.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Read me the last thing you have.</p></div></div></div><div class="screenplay"><p class="action">Miles pushes the mask up.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><p class="paren">(reading)</p><div class="lines"><p class="line">Payments, three years, unaccounted.</p><p class="line">Faculty of natural philosophy.</p><p class="line">Not approved.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Good.</p></div></div></div><div class="screenplay"><p class="action">Miles pulls the mask down. Simon has not written anything and does not want to.</p></div><div class="screenplay"><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">Is anybody actually worried about him?</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Nobody's worried. Nobody has been worried for</p><p class="line">three years. What they have is a number that won't</p><p class="line">reconcile, and they'd like it to reconcile by the</p><p class="line">end of the month.</p></div></div></div><div class="screenplay"><p class="action beat">Simon writes nothing down and does not stop looking at Victor.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">You're at the end of a corridor on the third</p><p class="line">floor. There's a storage room at the end of it and</p><p class="line">the door is locked from the inside.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">I hit the door.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">You may hit the door.</p></div></div></div><div class="screenplay"><p class="action">Miles rolls. It is not enough. Everybody at the table can see that it is not enough.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">The door is administrative. You'd need a form and</p><p class="line">the form is on the other side of it.</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Is that a joke?</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">No.</p></div></div></div><div class="screenplay"><p class="action">SIMON leans over to <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> and taps the sheet twice, near the top, where it says what a ranger is for.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">I investigate at the floor.</p></div></div></div><div class="screenplay"><p class="action beat">Victor stops.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Say that again.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">The corridor floor. If he's been in there, he came</p><p class="line">along the corridor. Is there anything on the</p><p class="line">floor?</p></div></div></div><div class="screenplay"><p class="action">Victor looks at his screen for a moment longer than the question needs.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Roll for it.</p></div></div></div><div class="screenplay"><p class="direction">Roll it. What the table rolled is in the box.</p></div><div><div class="roll" id="roll-corridor"><div class="roll-head"><span class="roll-label">TRACKING · THE THIRD-FLOOR CORRIDOR</span><span class="roll-dc">DC 12</span></div><div class="roll-bar"><button class="roll-go">Roll d20</button><span class="roll-value"></span></div><p class="roll-out"></p><div class="roll-bar"><button class="roll-rewrite" style="display:none;">Rewrite history</button><span class="roll-echo"></span></div><p class="roll-note"></p><p class="roll-foot">Echo: <span class="echo-count">0</span> · Score: <span class="score-count">0</span></p></div><script type="application/x-scittle">(if-let [mount (resolve (quote fedoras.ui.dice-ui/mount!))] (mount {:id "corridor", :dc 12, :outcomes {:fumble "{pc} rolls a one. The tin goes over and Derek picks the dice up off the floor.|@VICTOR|There's nothing on the floor. There's never been anything on the floor, and you know that the way you know your own name, and you stop looking.", :fail "{pc} rolls {result-mod} and Miles says the number out loud before anybody has asked him to.|@VICTOR|Dust, and it's even, and it's even all the way to the door. Which is worse than a footprint would be, and you couldn't tell me why yet.", :success "At the table, {pc} rolls a {result-mod}. The tin is loud on the folding table and everybody looks at it.|@VICTOR|Three years of dust and one track through it, going in, and nothing coming back out. The track is eleven minutes old.", :crit "{pc} rolls a twenty. Nobody at this table has rolled a twenty since March and Victor stands up.|@VICTOR|One track going in and nothing coming out. And there, under the door, where the dust is pressed flat: somebody has been sitting with their back against it, for a long time, and recently."}}) (js/console.error "fedoras: fedoras.ui.dice-ui/mount! is not on this page. Either the file that declares it was never shipped, or the namespace inside it does not match the path it was shipped from."))</script></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Three years and eleven minutes.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Yes.</p></div></div><div class="dialogue"><p class="character">DOCUMENTARIAN [MILES]</p><div class="lines"><p class="line">Hang on. Both of those can't be true at once, so</p><p class="line">one of them's new.</p></div></div></div><div class="screenplay"><p class="action beat">Victor takes a moment.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Now you're playing.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody says anything for a moment.</p></div><div class="screenplay"><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">I open the door.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">It opens. He's sitting on the floor with his back</p><p class="line">where the door was. He says he has been in here</p><p class="line">for three years and he would like to know why</p><p class="line">nobody came.</p></div></div><div class="dialogue"><p class="character">DOCUMENTARIAN [MILES]</p><div class="lines"><p class="line">He's been missing eleven minutes.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">He has.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Which one's true?</p></div></div></div><div class="screenplay"><p class="action beat">Victor puts his hand flat on the screen.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Both. That's the campaign.</p></div></div></div><div class="screenplay"><p class="action">The storage room is deeper than a storage room. Behind the man there are shelves, and behind the shelves, on the floor, there is a cage with a card wired to the front of it. The card has a date on it and nothing else.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">What's in the cage?</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Something that has been in a cage in a locked room</p><p class="line">for three years and is still alive, which ought to</p><p class="line">be your first question and isn't.</p></div></div><div class="dialogue"><p class="character">CLERIC [DEREK]</p><div class="lines"><p class="line">Don't open the cage.</p></div></div><div class="dialogue"><p class="character">PALADIN [SIMON]</p><div class="lines"><p class="line">Open the cage.</p></div></div><div class="dialogue"><p class="character">DOCUMENTARIAN [MILES]</p><p class="paren">(muffled)</p><div class="lines"><p class="line">Mmff mm mmfff mm mmffm.</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Mask.</p></div></div></div><div class="screenplay"><p class="action">Miles pushes the mask up.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DOCUMENTARIAN [MILES]</p><div class="lines"><p class="line">I said I can't record it as approved if nobody</p><p class="line">approved it.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Then record it as not approved.</p></div></div></div><div class="screenplay"><p class="action beat">Miles does, and enjoys it.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">I open the cage.</p></div></div></div><div class="screenplay"><p class="action">Victor sits back and lets it happen without asking for a roll, which he has not done once all evening.</p></div><div class="screenplay"><p class="direction">Roll for what comes out. The table's is in the box beneath.</p></div><div><script type="application/x-scittle">(ns fedoras.ui.companions
"The ranger's animal, in the page.
Loaded once by `fedoras.companions/table`, which then emits one line
handing over the roster, so the twenty arrival passages are passed as
Clojure rather than stringified into the markup."
(:require [clojure.string :as str]))
(defn- foot-note
"The line under the box."
[echo]
(cond
(zero? echo) "Written on the sheet in ink."
(< echo 3) "Written on the sheet in ink. There is something under it."
:else (str "Written on the sheet in ink, over " echo " other things.")))
(defn- shown-name
[{:keys [name animal]}]
(cond
(str/blank? animal) (str/upper-case name)
(= (str/lower-case name) (str/lower-case animal)) (str/upper-case name)
:else (str (str/upper-case name) " · " (str/lower-case animal))))
(defn- kind-line
"What the ledger would have written in the other column. Only shown
once the roster has landed."
[{:keys [taxonomy plural collective]}]
(when-not (str/blank? taxonomy)
(str (str/capitalize taxonomy)
(when-not (str/blank? collective)
(str ". More than one is " (str/lower-case collective) "."))
(when (and (str/blank? collective) (not (str/blank? plural)))
(str ". Plural: " (str/lower-case plural) ".")))))
(defn- clear! [node]
(fedoras.reader/text! node ".companion-roll" "")
(fedoras.reader/text! node ".companion-name" "")
(fedoras.reader/text! node ".companion-kind" "")
(fedoras.reader/text! node ".companion-intro" "")
(fedoras.reader/enable! node ".companion-go" true "Roll for your animal")
(fedoras.reader/enable! node ".companion-reroll" false nil)
(fedoras.reader/show! node ".companion-naming" false))
(defn- fill! [node roster {:keys [n given] :as c}]
(let [intro (get-in roster [n :intro] "")
input (fedoras.reader/q node ".companion-input")]
(fedoras.reader/enable! node ".companion-go" false "Rolled")
(fedoras.reader/enable! node ".companion-reroll" true nil)
(fedoras.reader/show! node ".companion-naming" true)
;; not while they are typing in it. this runs on every render, and a
;; render can be triggered by something else on the page.
(when (and input (not= input (.-activeElement js/document)))
(set! (.-value input) (or given "")))
(fedoras.reader/text! node ".companion-roll" (str "d20 → " n))
(fedoras.reader/text! node ".companion-name" (shown-name c))
(fedoras.reader/text! node ".companion-kind" (or (kind-line c) ""))
;; the shared filler, so an arrival passage can use every token a
;; scene can. it used to know about {pc} and nothing else.
(fedoras.reader/text! node ".companion-intro" (fedoras.reader/fill intro))))
(defn mount!
"Wire the roll box. `roster` is the twenty entries, as Clojure."
[roster]
(fedoras.reader/widget
::companion "companion"
(fn [node]
(if-let [c (fedoras.reader/companion)] (fill! node roster c) (clear! node))
(fedoras.reader/text! node ".roll-foot" (foot-note (fedoras.reader/echo)))
(fedoras.reader/on-click! node ".companion-go"
(fn [_]
(when-not (fedoras.reader/companion)
(fedoras.reader/set-companion! (inc (rand-int 20)) ""))))
(fedoras.reader/on-click! node ".companion-reroll"
(fn [_]
(fedoras.reader/set-echo! (inc (fedoras.reader/echo)))
(fedoras.reader/set-companion! (inc (rand-int 20)) "")))
(fedoras.reader/on-click! node ".companion-name-go"
(fn [_]
(when-let [cur (fedoras.reader/companion)]
(let [given (.-value (fedoras.reader/q node ".companion-input"))]
(fedoras.reader/set-companion! (:n cur) given)
;; naming it is a deed. it is not a large one and nothing
;; anywhere says it happened, which is the arrangement.
(when-not (str/blank? given)
(fedoras.reader/did! :named-it)))))))))</script><div class="roll" id="companion"><div class="roll-head"><span class="roll-label">COMPANION</span><span class="roll-dc">d20</span></div><div class="roll-bar"><button class="roll-go companion-go">Roll for your animal</button><button class="roll-rewrite companion-reroll" disabled="disabled">Roll again — rewrite history</button><span class="roll-value companion-roll"></span></div><p class="companion-name"></p><p class="companion-kind"></p><p class="companion-intro"></p><div class="companion-naming name-inputs" style="display:none;"><input class="companion-input" placeholder="name it" spellcheck="false" type="text" /><button class="roll-go companion-name-go">Write it on the sheet</button></div><p class="roll-foot">Written on the sheet in ink.</p></div><script type="application/x-scittle">(if-let [mount (resolve (quote fedoras.ui.companions/mount!))] (mount {1 {:animal "Pigeon", :plural "pigeons", :taxonomy "bird", :collective "kit", :other-names ["dusty wings" "coooo"], :intro "The cage is large enough for a man to stand up in, and Victor lets the table take in the size of it before the door swings open. A pigeon walks out. It is the ordinary grey kind, the colour of the pavement it came from, with one foot worse than the other, and it walks with its head bobbing, stopping at the Ranger's boot, and looking him up and down with one orange eye. There is a ring on its leg. Simon asks if his character can read the ring. Victor nods his assent, like a pidgeon; cooing loudly enough for the whole building to hear \"- a letter and then six figures\", Simon says. Miles jots it down with something resembling a twisted sort of joy.Derek says pigeons can tell one human face from another and keep a face in mind for years.Then the pigeon eats the card off the front of the cage, which Victor allows, and then Miles asks what the date was, and Victor says it is in the pigeon now.Miles, slightly unsatisfied, sneers at {pc} while Victor continues on..."}, 2 {:animal "Giant Centipede", :plural "centipedes", :taxonomy "arthropod", :collective "colony", :other-names ["clicker" "legs gooo"], :intro "The cage is a wire box about the size of a large carryon. Victor, who has spent a good portion of the evening describing dust, spends one sentence this 'cage'. Something in it moves when the door opens. Twitching. A head comes out, antennae, the first pair of legs, and then a segment. Another, and another, the box would seemingly have to be empty long before the centipede has finished leaving it, yet Victor keeps going. It keeps coming, the colour of a varnished floor, the legs moving down the length of it in mesmerising waves, until the front end is at the door of the storage room and the back end is still coming out of a box with nothing left in it. Simon says that the Paladin thinks that its beautiful. Victor says the colour is a warning. Simon says he knows."}, 3 {:animal "Marmot", :plural "marmots", :taxonomy "rodent", :collective "madness", :other-names ["the alarm" "the fat rat"], :intro "The cage opens and nothing comes out of it for long enough that Derek asks whether it is empty. Then a marmot comes out. It is the size, and roughly the shape, of a loaf - brown and going blond at the edges, and although it has been in a cage in a locked room for three years it has plainly eaten well the entire time and seems well rested. It sits up on its hind legs to look at the Ranger. Sitting up, it is still quite short. Miles asks what a marmot is good for. Victor says it is for sitting up and looking at things, and for chirping when the looking goes badly, and demonstrates the chirp. It is piercing and lasts too long. He has practised it. Someone from the room next door puts their head round to ask whether everything is all right, and Victor says it is nothing to worry about. They go, and Miles writes down what a marmot is for."}, 4 {:animal "Cat", :plural "cats", :taxonomy "felid", :collective "clowder", :other-names ["the cat" "cat"], :intro "The cage opens and the cat stays at the back of it. Head low, Victor describes it deciding on its next move cautiously. It is a black cat, fully grown and with a long and slender body, yet short in temper - with an ear that has seen at least one argument. It stays where it is for as long as anybody is watching. The Paladin looks away first. When he looks back the cat is out, and up on a shelf, sitting with its tail over its feet as though it has been there all evening and the party is late. The Ranger holds out a hand. The cat considers the hand at length. Miles writes NOT APPROVED and does not say by whom. Twice before the end of the scene the cat gets down, goes to the door of the storage room, and sits facing the empty corridor with its ears forward, seeminglylistening for something that {pc} fails to perceive."}, 5 {:animal "Turkey", :plural "turkeys", :taxonomy "fowl", :collective "rafter", :other-names ["thanksgiving dinner for non vegans" "gobbler"], :intro "The cage opens and the turkey comes out of it already displaying. It is enormous, bronze and black and bald at the head with a gigantic bouncing red comb. While Victor is describing the head it goes blue (which is what a turkey's head does when its feelings change - a turkey's feelings change all the time), and it warbles at the cage door as the door swings open. It gobbles at the man on the floor. When Derek sets his mug down on the folding table in Room 3C, Victor, in character, warbles at the mug. Derek picks the mug up again. Miles asks what it is for. Victor says it answers every sound it hears, so anything that wants to come along the corridor quietly will have to do it with a turkey answering. Miles writes TURKEY (RESPOND WITH WARBLES)."}, 6 {:animal "Giant Badger", :plural "badgers", :taxonomy "mustelid", :collective "cete", :other-names ["angry grandpa" "stripes"], :intro "The cage door swings open and the badger ignores it, having dug out through the bottom of the cage instead. Victor describes all of it: the claws, the wire mesh peeling back like foil. Then the floorboards and whatever was under them. The open door stands a foot to the badger's left throughout. The Paladin points out the door. Victor says the badger is aware of their presence It comes up again a yard in front of the Ranger, broad and low and striped like a road, shakes the dust off its back, and looks at him with contempt reserved for an animal that has always come and gone by its own arrangements. It eventually leaves and goes back down the hole. The storage room has one more way out than it did, and Victor leaves it off the pizza box. Miles draws it in himself, small, in the corner of his sheet, smirking to himselfwith a sense of misplaced pride."}, 7 {:animal "Giant Weasel", :plural "weasels", :taxonomy "mustelid", :collective "sneak", :other-names ["the scout" "slenderman" "the ermine"], :intro "The cage opens and the weasel is out before the door has finished swinging: into the shelves and out, up the man on the floor and down his other side, twice round the Ranger and back to the cage, in the time it takes Victor to say that the door is open. It is long, brown on top and white underneath, built like a draught excluder with teeth. It has the man's spectacles. Nobody saw it take them, yet the man sits blinking at a room he has been staring at for the last three years and can no longer see. The weasel lays the spectacles at the Ranger's feet with great ceremony, and backs off, watching to see whether he is pleased. Miles asks whether they can give them back. Victor says they can try but will need to roll for it. Miles writes thatdown, but does not elect to return the glasses, and insteadalso writes those down as well."}, 8 {:animal "Fox", :plural "foxes", :taxonomy "canid", :collective "skulk", :other-names ["foxxxy" "spry foot"], :intro "The Ranger lifts the latch and it comes up easily, too easily, and Victor stops to say so. The inside of the latch is worn bright against the rust. It has been lifted before, many times, from the inside, and let fall again. The fox steps out as though the evening had been arranged for it. It is red and narrower than a fox in a picture, white at the tip of the tail, black stockings to the knee, and it walks once round the Ranger at the distance of a guest who has been introduced to him at a party and has not yet decided. Then it goes and sits at the edge of the light from the doorway, where the dust begins, and looks back at him. Derek asks why it never left. Victor says it could have gone any time it liked. Simon asks what it was waiting for, and Victor looks for a moment at the line near the bottom of {pc}'s sheet, and says that they'll have tofigure that out. Miles says the Documentarian logs it as an anomaly."}, 9 {:animal "Wolverine", :plural "wolverines", :taxonomy "mustelid", :collective "gang", :other-names ["the small trouble"], :intro "The bars of the cage have been chewed bright on the inside, every one of them, all the way round, as high as something could reach standing up. There are tooth marks in the lock. The cage has held, and Victor growls as loudly as he can. When the door opens the wolverine stays where it is and looks at the Ranger through the gap: low and broad, with a pale band down each side like a saddle and the face of a small bear that has been told something it did not like. Victor says it is deciding whether he is the one who put it there. It comes out, walks past him, and marks the corner of the storage room - the smell covering a 30ft circle according to Victor. Derek says that the Cleric pours out his drink. The wolverine lies down across the doorway with its chin on its paws, and Victor says it has decided to accompany them after all."}, 10 {:animal "Dog", :plural "dogs", :taxonomy "canid", :collective "pack", :other-names ["the dog" "the ordinary dog"], :intro "The cage opens and a dog comes out of it wagging. An ordinary dog, brown and white, middling in size, its ears undecided. It has been in a locked room for three years and it comes out at a run anyway, and skids to a stop in front of the Ranger, and sits, very correctly, the way somebody once taught it to. Simon says \"aww\". It has a collar, and a tag that has been filed flat on the side where the name was. Victor lets them turn it over. On the back is a line that says IF FOUND, and after that the metal has been filed smooth as well.Miles notes that the documentarian logs the missingdog name as an anomaly and suggests an interim name ofMarcus, to which Derek's ear's perk up and he shouts:\"you ought to be one of those who, in a sense, are unconscious of the good they do!!!!\" The documentarian writes that down as well, under \"miscellania\""}, 11 {:animal "Python", :plural "pythons", :taxonomy "serpent", :collective "knot", :other-names ["ssSSssSsss" "coilssss"], :intro "There are coils, and coils under those. It is some time before anybody can say where the python begins, and by then it has begun to come out. It pours over the lip of the cage and onto the floor, and Victor describes it at the speed it happens, so the table waits a long time for the end of the sentence. The man on the floor says it has eaten once. Miles asks when. The man says in the first year. At last the head comes up, and turns. The labial pits along its lip go looking for the warmest thing nearby. It sinks its teeth into a shadowy form inthe shape of a dog that Victor describes as materializing as it latches on. It buys them a free round in the coming encounter. Victor says a python keeps what it takes."}, 12 {:animal "Mule", :plural "mules", :taxonomy "equid", :collective "barren", :other-names ["dapple" "HeeHaw :("], :intro "The cage is plainly too small for a mule, and yet there it is: a mule. Victor offers no explanation. The mule offers a small, reluctant bray, which Victor reproduces by screeching through pursed lips. The Ranger opens the door and stands aside, and the mule looks at the open door, and at the Ranger, and lowers its head slightly, and stays where it is. The Cleric pulls. The Paladin offers it something from his pocket, which Victor rules is a carrot The mule eats the carrot and stays where it is. Miles asks whether a companion that will not leave its cage counts as a companion, and Victor says it counts from the moment the door opens. When everybody has given up and turned round, the mule is standing behind them in the middle of the storage room with panniers on, and in the panniers include everything that was on the shelves. It has been in a locked room for three years, and at some point in those three years it packed."}, 13 {:animal "Lynx", :plural "lynxes", :taxonomy "felid", :collective "clowder", :other-names ["tufts" "yelper"], :intro "The cage opens and the lynx steps out, long in the leg, with feet far too big for it, the size of a man's hands, and tufts on its ears. It is thin. Victor says it is at the bottom of its cycle. Miles, of all people, knows what that means, and explains it with his pencil. For about a hundred years a fur company kept account books of every pelt it bought, and when somebody finally drew the lynx pelts against the hare pelts, the lynx rose and fell every ten years or so, a little behind the hares, like a tide. The most famous animal in ecology was found in a ledger. Miles is visibly moved. The lynx looks at the ledger in his hands as though it knows exactly what ledgers are for. Miles writes LYNX, and the date, and underneath, for the cycle, and makes a small mark in the margin to be checked in ten years."}, 14 {:animal "Ape", :plural "apes", :taxonomy "primate", :collective "shrewdness", :other-names ["treecrawler"], :intro "The cage opens and an arm comes out first, a long one, and the hand on the end of it takes hold of the door frame and pulls the rest of the ape out after it. It is big and red-brown and unhurried, with the face of an uncle who expected more of you Then it looks at the Documentarian. Victor holds his hand out across the table. Miles, after a long moment, takes one of his two pencils and puts it in Victor's hand, and Victor passes it to {pc} and says the ape has it now, and that it knows how to write. Miles says that only the documentarian is permitted to write. Victor agrees that this has always been the rule, but still does not ask for the pencil back, and Miles marks it in the Ranger's Companion's inventory.Simon says there was an orangutan in a zoo that kept picking its lock with a length of wire and hid the wire in its mouth so the keepers could never find it. The ape opens its mouth. There is a bent wire in its cheek. Victor lets that land. Then the ape gets up and puts one very long hand on the Ranger's shoulder, as if it has been waiting for somebody going the same way, and Miles writes APE (HAS WIRE)."}, 15 {:animal "Horse", :plural "horses", :taxonomy "equid", :collective "harras", :other-names ["rocinante" "the one nobody rides"], :intro "The cage opens, a horse comes out of it, and the storage room is taller than it was. Victor accounts for neither. It is a grey, tall and old, gone white at the muzzle and the knees, and it is saddled. The leather is dark with oil, the buckles are bright, and the girth is done up to the right hole. Somebody has been looking after this saddle for three years in a locked room. On the breastplate is the crest of the Collegium. The Cleric leans in to look, and Derek says that is the wrong crest. Victor says it is the crest. Derek says the motto is wrong. Victor reads the motto out in a language none of them recognize, and moves on. The horse comes to the Ranger, folds one foreleg, and bows with its nose to the floor, as horses were once taught to bow to kings. Simon, whose paladin cannot call a horse of his own until fifth level (which he is painfully aware of), says nothing, and thoughtfully lines his dice up again by size. Victor calls it by a name nobody at the table has heard. Derek asks where the name is from. Victor says \"an approved book\" Derek asks which book, and Victor says it is not oftheir concern, and moves on so quickly that Miles has to ask him to repeat the name, and Victor declines, so Miles writes HORSE (NAME WITHHELD)."}, 16 {:animal "Moose", :plural "moose", :taxonomy "cervid", :collective "herd", :other-names ["its a giant moose"], :intro "The cage opens and there is a moose in it, facing the back. Victor sets out the difficulty: the antlers are wider than the door of the cage. They are wider than the door of the storage room. The moose cannot turn round in the cage, and if it could, it still could not get out of it, and thereafter, even if it got out - it still couldn't leave. Victor explains, without being asked, that somebody \"must have built the cage round it\". The table sets to work. Derek rolls a sixteen for Strength and the Cleric takes the cage apart. The Paladin measures the doorway with his arms. Miles draws the moose on the pizza box to scale, and it covers four squares and the door, and he looks at that for a long while. While they are still arguing about the door, the moose tips its head sideways, as moose do among trees, and backs out of the cage, and backs out of the storage room, and stands in the corridor looking in at them with enormous calm."}, 17 {:animal "Wyrmling", :plural "wyrmlings", :taxonomy "draconic", :collective "clutch", :other-names ["the wyrmling" "scales"], :intro "The cage is the only thing in the storage room with no dust on it. Victor says so before the Ranger opens it and waits for somebody to ask why. Nobody does, so the Ranger opens it. Heat comes out, and nothing else. The wyrmling is asleep, curled nose to tail on the floor of the cage, about the size of a spaniel, and the card on the front of the cage has gone brown at the edges. Derek asks what colour it is. Everybody at the table knows why he is asking. Victor says it is prismatic and difficult to tell in this light. Derek asks again, differently. Victor says its scales are the colour of whatever light is on them, and there is not much light at present. Before anybody has decided how to lift it, it crawls out of the cage in its sleep and into the Ranger's pack, and settles against his back, and he is warm for the first time since the corridor. For the rest of the evening Derek sits at the far end of the table from {pc}, and asks about the colour at intervals, and gets the same answer."}, 18 {:animal "Falcon", :plural "falcons", :taxonomy "raptor", :collective "cast", :other-names ["the raptor" "true sight"], :intro "On a perch at the back of the cage there is a falcon in a hood. The Ranger takes it off, and Victor says that the falcon doesn't look at him. It steps out onto the top of the cage, and stretches both wings as farrrrr as possible (as Victor emulates)straight out to the sides, and holds them there. Forty-two inches, tip to tip. Miles found that number earlier because Victor made him look it up, and Miles reads it off his sheet now without being asked, and Victor does not thank him for it, which Miles notes.\n\nThe Ranger holds out his arm. He has not done this before and isn't sure of the best way to approach it. The falcon looks at the arm, looks at him, steps onto it, and closes its talons through the sleeve and into the forearm underneath. Victor SCREAMS like a falcon (he has clearlybeen practising) - long, shrill, and piercing, and holds it for a full three seconds. Simon laughs. Miles does not. Derek looks at the floor. The falcon does not make the noise back. It is busy. It is holding on."}, 19 {:animal "Wolf", :plural "wolves", :taxonomy "canid", :collective "pack", :other-names ["the wolf" "the packleader"], :intro "The cage opens, and the wolf a wolf comes bounding out without , hesitation, going straight past the Ranger to the door of the storage room, and and stands with its head low, and a growl to match.It ranks them, pacing between each of them in turn, deciding where they stand, and Victor narrates the deciding: the man on the floor first, then the Ranger, then the Paladin, then the Cleric, and the Documentarian last of all. Miles objects. Derek says that the whole business of wolves ranking one another came from a study of wolves kept in captivity, and that the man who made it famous spent years asking people to stop repeating it, because wolves in the wild live in families and do nothing of the kind. Victor points out that this wolf has been in a cage for three years. Miles asks whether that means the ranking stands. Victor says in here it does, and the wolf positions itself between theRanger and the door.It doesn't pay particular attention to them, because it has already decided that they are not the ones to be paying attention to. Miles asks \"What it is doing?\", and Victor says it is waiting for something on the other side of the door. After a moment he adds that whatever it is waiting for has been waiting too, and that the wolf has known about it longer than any of them have.\n\nThe wolf howls, and Victor howls with it - a long one, well practiced - thrown back into the room so that the whole building has to hear it, then stops, and cups his hand to his ear, and looks at Simon, and says: \"Well?\" ... No response. Victor refuses to continue the game until everyone acquiesces to howling as a pack. After enough back and forth, the whole tablegrugingly accepts and low effort howls finally reverberated back, to Victor's delight."}, 20 {:animal "Bear", :plural "bears", :taxonomy "ursid", :collective "sloth", :other-names ["the bear" "the citadel"], :intro "The floor of the cage is worn. There is a groove in it in the shape of an eight, and the bear is walking the groove, round and across and round, and when the door opens it keeps walking. Victor traces the eight on the pizza box with his finger and keeps tracing it, round and across and round, while he talks. Derek says bears in cages do that, and that some of them go on doing it after they are let out. The bear comes out of the cage still walking the eight and walks it on the floor of the storage room, round the man on the floor and across and round the Ranger, and every time it passes the Ranger it slows, a little, and puts its nose to his hand. Simon asks whether it will stop. Victor keeps his finger going and says not tonight."}}) (js/console.error "fedoras: fedoras.ui.companions/mount! is not on this page. Either the file that declares it was never shipped, or the namespace inside it does not match the path it was shipped from."))</script></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">It came out on its own. Nobody told it to do that.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">It's yours now. It doesn't need telling.</p></div></div></div><div class="screenplay"><p class="action">Something comes up the corridor behind them. Victor takes his time over it: the sound first, then the size of it, then the smell, and only afterwards the shape. It is a dog. It is the wrong height at the shoulder and it has been made by whatever makes forms rather than by whatever makes dogs. It seems to be... rippling?</p></div><div><div class="session" id="session-one"><p class="session-who"></p><p class="session-text"></p></div><script type="application/x-scittle">(fedoras.reader/on-render! :session/one (fn [] (when-let [node (fedoras.reader/el "session-one")] (if-let [c (fedoras.reader/companion)] (do (fedoras.reader/text! node ".session-who" (clojure.string/upper-case (:name c))) (fedoras.reader/text! node ".session-text" (get {1 "Two of the hounds come along the corridor making for the ledger, and the pigeon walks out into the middle of the floor, because that is where the crumbs would be. The front hound stops. It is a page-dog, and it does what a page-dog does, which is to look at a thing and imprint it on its own page so that the record can go back to whoever sent it. It looks at the pigeon. The pigeon takes one step sideways. The hound imprints the pigeon. The pigeon takes another step. The hound imprints the pigeon again, because the first imprint is now out of date.\n\nThe pigeon walks up the hound's foreleg, onto its back, and stands there, looking round at the corridor from a slightly better height. The hound stands very still. It cannot imprint the pigeon, because the pigeon is behind its eyes, and it cannot imprint itself, because a page-dog has never been asked to record its own back. The second hound comes up beside it, looks at the first hound, and imprints that. The first hound does not turn round. It does not know how.\n\nMiles writes HOUND DETAINED BY PIGEON. Victor looks at the entry for a long moment, and approves it. It is the only thing Victor approves all evening.", 2 "The centipede goes up the front hound's foreleg and in at the shoulder, between one page and the next, and the hound stops. It stands very straight, the way you stand when there is something in your shirt.\n\nThe hound is a record in progress. Every page inside it has an imprint on it — things it has looked at, things it has sniffed, things it has been told to remember — and the centipede is walking through all of them, in order, and eating as it goes. Victor starts reading the imprints out as the centipede passes them, because the numbers are in the corners and he can see them: forty-three, forty-seven, nine, a hundred and five. He gets the joke of it about four numbers in, and stops, and the centipede does not stop, because the centipede is not doing a joke.\n\nBy the time it comes out at the tail the hound has no record left in it. It sits down in the corridor and begins to look at things, so that it can make a new record, which is what a page-dog does when its pages are empty. It is still looking at things when Victor calls tea. It is still looking at them when they come back. Miles asks what it has got to. Victor says it keeps imprinting the same stretch of corridor over and over, because it has nothing in it to tell it that it has already been here.", 3 "The marmot sits up in the middle of the corridor to look at the hounds. The looking goes badly, so it whistles. The front hound stops. It was assembled by something that has read a great deal about dogs, and everything it has read agrees on what a dog does when it is whistled for. It turns, and trots back to the marmot, and sits down beside it at the wrong height, facing the same way. It imprints the marmot. Then it imprints the wall the marmot is looking at, because a page-dog records what is in front of it and the marmot is now in front of a wall.\n\nVictor lets this stand until the table starts laughing, and then for a good while after the laughing stops. The marmot has a dog. It whistles again, twice, short, and the hound gets up and goes off down the corridor at a trot, to record whatever a whistle tells a dog to record, which neither the hound nor Victor nor the module has ever specified. Miles asks how he is meant to record that. Victor says accurately.", 4 "The cat was at the door before the hounds were in the building. When the front hound reaches the storage room Miles has the ledger open on the floor between his knees, and the cat gets up, crosses the corridor without hurrying, steps onto the open ledger, turns round twice, and lies down on it.\n\nThe hound has come to record the ledger. The cat is on the ledger. It walks up, lowers its head, and imprints the cat instead, because a page-dog records what it can see and what it can see is a cat. Victor lays it out in exactly those words and then stops talking, because everybody at the table has at some point needed something a cat was lying on. The hound tries the next page. The cat lays one paw across its muzzle, claws in, as you might keep a hand on a letter to stop it blowing away, and leaves it there.\n\nIt stays like that for twenty minutes. At the end of them the hound goes, with a record that contains one cat and nothing else, and the cat is asleep, and Miles writes the rest of the evening on the back of his character sheet.", 5 "The first hound barks once at the top of the stairs. Its bark is the sound of a rubber stamp coming down on a desk. The turkey gobbles back. The hound stamps again. The turkey gobbles again, louder, and fans its tail, and its head goes blue to white to red in the time it takes Victor to say so.\n\nEach time the hound stamps, the turkey answers, and each time the turkey answers the hound has to stamp again, because a stamp that has been answered has not been stamped. It imprints the turkey. It imprints the answer. Then it has to imprint the imprint, because the imprint is now out of date, and then the imprint of the imprint, and it is going to be doing this for a long time. Victor does both noises, for some time. The second hound, which is further back, turns round and goes home with a page that says only TURKEY.\n\nThen the first hound comes forward, and the turkey goes to meet it low with its wings out, the way turkeys go at people in car parks, and the hound — which has come up three floors of the Collegium without once being contradicted — backs away. Victor finishes the encounter in a whisper, because the turkey answers him too.", 6 "The badger is under the corridor before the first hound reaches it. Victor says so, and then describes the corridor as though there were nothing under it, which is worse. The hound comes on with its nose down. Halfway along, the boards under its forefeet sag, and part, and it goes in to the shoulder, and something below takes hold of it, and it goes in the rest of the way.\n\nThe boards settle. There is a scraping, going away, then a scraping, coming back, then quiet. The badger comes up beside the storage room door with plaster in its fur, sneezes, and sits. The second hound stops at the edge of the hole and imprints the hole. Then it turns round and goes back the way it came, because a page-dog records what it has found, and what it has found is that the corridor has a way down. Miles asks where the first hound is. Victor says under the floor. Miles asks where under the floor. Victor says with the rest of the records, and Miles, slowly, writes FILED.", 7 "When the hounds come round the corner the weasel begins to dance. Derek knows the name for it and says it: the war dance. The weasel leaps sideways, and backwards, and straight up, twists in the air and lands facing somewhere else, and does it again, faster, back arched and mouth open, its whole length thrown about as though something invisible has it by the tail.\n\nVictor stands up to show them. He is broad and the hat is tall, and the war dance, performed by Victor in Room 3C, takes the full width of the room, and at each position the hat arrives a moment after he does. Simon cannot look away. Neither can the hounds. The front hound is trying to imprint the weasel and it cannot, because the weasel is never in the same place as the last page it filled. By the time the weasel reaches it, the hound has thirty pages of nothing in particular and one at the end, in progress, with the weasel at its throat.", 8 "The fox gets to the ledger before the hounds do, which is the hounds' own trick, and takes it out of Miles's hands so gently that his hands are still holding the shape of it. Then it runs. It goes past the front hound close enough to touch, and the hound turns and goes after it, because the ledger is what it came for and the ledger is leaving down the corridor in the mouth of a fox. The second hound imprints the fox, and the ledger, and the corridor, and turns and goes back to tell somebody.\n\nThe first goes down the stairwell after the fox. Victor calls the chase by floors: third, second, first, and then a floor the building does not have. The fox comes back alone, with a page in its mouth that is not the ledger page and is not the hound's. It sets the page down at {pc}'s feet. Miles reads it. It says the fox was here, and it says where the fox went, and it is in a hand none of them recognise, and it is the first thing all evening that was not written by someone at the table.", 9 "The wolverine is lying across the doorway when the front hound arrives, and it goes on lying there until the hound is close enough to make a mistake. Then it takes the hound's front leg off at the shoulder — and a leg, on a page-dog, is pages — turns, and carries the leg into the storage room, and puts it under the bottom shelf, where nobody can reach it.\n\nThe hound stands in the corridor missing a leg. It cannot proceed. It was assembled to proceed, and the pages it needs are under a shelf in another room, and it was not assembled to look under shelves. It stands there. It keeps standing there. The second hound comes up beside it, imprints it, and waits to see what the first hound will do, which turns out to be nothing, indefinitely. The wolverine comes back out, sits down in the doorway with its chin on its paws, and looks at Miles. At the ledger in Miles's lap. At the paper. Miles holds the ledger against his chest. Victor lets them listen to the strip light.", 10 "The dog hears the hounds before anybody else does and goes to meet them with its tail up, as one dog goes to meet another in a park. A yard away it stops and bows: elbows down, back end up, the invitation every dog knows. The front hound stops as well. It was assembled out of everything ever written down about dogs, and none of it mentioned this. It studies the dog. Then it lowers its front end, stiffly, at the wrong height, into something with the outline of a bow. It imprints the bow. The dog, delighted, bows back. The hound imprints the answer, because the answer is a bow, and a bow that has been answered is not the same bow. So it bows again. And the dog bows back.\n\nFor a minute of table time the two of them bow to each other in the corridor, and Victor describes every bow, and nobody at the table says a word. The second hound, further back, walks past the whole thing without looking, because it has been sent for the ledger, and it is the only page-dog in the corridor that is. It reaches Miles while the first hound and the dog are still bowing, and Miles has to decide whether to shut the ledger or keep watching.", 11 "The python is on the beam over the corridor. Nobody saw it go up there. Victor says it has been there since the tea. The front hound passes underneath with its nose to the floor, cold as paper is cold, and the python, which has spent the evening looking for warmth, lets it go by. Then the hound reaches Miles, who is warm, and lowers its head to the ledger in Miles's lap, and for a moment the cold thing and the warm thing are in the same place.\n\nThe python comes down on both. It lets go of Miles at once. It keeps the hound. Victor describes the first fold, and the second, and the sound of a page being folded along a line where it has never been creased. Derek says paper can only be folded seven times. Victor says yes, and describes the eighth. The second hound stops at the bottom of the corridor and imprints the python, and imprints the hound inside it, and turns and goes back, because a page-dog that has seen a thing folded eight times has seen enough to report. When the python lets go at last, the hound opens out slowly on the floor with the creases still in it, and every imprint inside it is now on the wrong side of a fold, and it goes back down the corridor unable to say a single thing it came to say.", 12 "The hounds have come for the ledger, and the ledger is in the mule's pannier, because by now everything is. The front hound goes to the mule's left side. The mule turns, so that the hound is at its back end. The hound goes round to the right. The mule turns. The hound goes round again and the mule turns again, patiently, on the spot, like a turntable, and they go round like that in the corridor while Victor narrates each quarter-turn in the same level voice, until Simon has to put his head down on the table.\n\nThe second hound is standing back, imprinting every quarter. It has been told to find the ledger, and what it has found is a mule, from four angles, and it has no way of knowing that the four angles are the same mule, so its report is going to be very long and is not going to mention a ledger at all. Then Victor rolls a die in front of the screen, in the open, for the first time all evening. It comes up twenty. He describes the kick. There is only one. The first hound leaves the corridor by the far end at some height above the floor. The mule settles, and declines to move from the spot for the rest of the night, and the party has to unload it where it stands. Miles writes MULE. DECLINED TO PROCEED.", 13 "The lynx is on top of the door frame when the hounds come along the corridor. The first hound passes under it, and the lynx drops. It is on the hound's back with its teeth in the throat before Victor has finished the sentence about where it went, and Victor finishes the sentence anyway.\n\nAlong the underside of the hound's throat, from one side to the other, there is a line of small holes. Victor mentions them only now, as though they had been there all along: a perforation, of the sort a form has where you are meant to tear. The lynx tears along it, cleanly, the whole way. The piece that comes away is the stub, the part at the bottom of a page where the imprint is signed, and the lynx carries it over and sets it down in front of Miles. The rest of the hound stands in the corridor for a moment, unsigned, and then goes back the way it came. The second hound imprints the stub. The lynx watches it do it. By the time Miles has filed the stub the lynx is on top of the door frame again, washing a forepaw, and the second hound is walking very slowly backwards down the corridor, keeping the door frame in view.", 14 "The hounds come up the corridor with their heads low, making for whoever in the building is about to write something down, and that is the ape. The ape looks at the front hound, and at the pencil, and at the hound. Then it takes the hound by the scruff, turns it over, and smooths it out flat on the floor, as you would smooth a form on a counter, and begins to fill it in.\n\nVictor describes the tip of the ape's tongue at the corner of its mouth, and the pencil moving from box to box. He does not describe what it writes. When it has finished, the ape signs at the bottom of the hound, and the hound gets up. A completed form has somewhere to be. It sets off back down the corridor at a brisk trot, towards whatever office receives it, and does not look round. The second hound watches it go. Then it looks at the ape, and the ape looks back at it, and the ape is still holding the pencil. The second hound turns round and follows the first, and neither of them reports anything else for the rest of the evening. Miles asks what the ape wrote. Victor says it must have been true, or the hounds would have stayed.", 15 "The horse comes up the corridor to meet the hounds. There is not room for it. It comes anyway, ears flat, tail swinging, its shoulders touching both walls. Victor stands. He names what the horse does next, because there is a name for it: a capriole. The horse leaves the ground with all four feet at once, in a corridor on the third floor, and at the top of the leap it kicks out behind with both hind legs, level, the kick the old riding schools still teach long after the battles it was meant for.\n\nDerek knows the word from a film and mouths it along with him. At the top of the leap the front hound is behind the horse. Afterwards the front hound is at the far end of the corridor in a number of pieces, like a drawer of files dropped down a stairwell, and the pages have come apart in flight and are scattered the whole length of the corridor, each one an imprint of a different moment of a horse, and none of them in order. The second hound walks the corridor and picks them up. It is still picking them up when the party leaves. The horse comes down. Plaster sifts from the ceiling where its ears touched. It stands blowing, and then turns its head and waits for {pc} to get on.", 16 "The moose is in the corridor facing the party when the hounds come up the stairs behind it. It cannot turn round. The corridor will not let it. The whole table works this out at once, and then works out the only other thing a moose in a corridor can do.\n\nIt reverses. Derek, without being asked, makes the noise a lorry makes when it is reversing, and keeps making it, and Victor lets him, and narrates over the top: the moose going backwards down the corridor at speed, blind, antlers scraping both walls and bringing the plaster down, and the hounds at the far end looking up at a very large animal coming towards them from the wrong end. The beeping stops when the moose does. There is a long pause. The moose, having backed the whole length of the corridor, stands at the far end, still facing the party, entirely calm. Somewhere underneath it is the first hound, and its last imprint, on the last page inside it, is a moose coming at it from the wrong end. The second hound imprints the whole of it, including the moose, including the corridor, including the noise, which Victor does once more for the record.", 17 "The hounds come up the corridor and stop a yard short of {pc}, and lower their heads, and sniff at the pack. The wyrmling opens one eye. Victor stops talking. He takes the hat off, which nobody at the table has ever seen him do, and sets it on the floor beside his chair, and then he describes the breath. It is short. It is fire.\n\nThe front hound is paper and three years of dust, and it goes up the way a letter goes up in a grate, all at once, bright at the edges, and then it is ash standing in the shape of a dog. Derek says fire could mean red, or gold, or brass. Victor says yes. The second hound imprints the fire, and the ash, and turns and leaves, and Victor does not stop it. The eye closes and the wyrmling goes back to sleep. Then the ash moves. It stands up in the shape of the hound, grey and soft and exact, turns round, and goes back down the corridor the way it came, and it leaves no footprints in the dust. It has no imprints left in it, because it has no pages, and it is going home to report whatever an ash hound reports, which is not going to be anything useful to anybody.", 18 "As soon as the hounds are on the stairs the falcon goes up the stairwell, up past four floors of banister and out of the light, and for a while there is nothing but the front hound coming along the corridor towards Miles. Then Victor does the noise with his mouth. It starts high and comes down the stairwell faster than anything can fall.\n\nThe falcon hits the front hound with a closed foot at the bottom of the stoop, and the hound comes apart. It goes up like a ream of paper thrown at a ceiling and comes down over the whole corridor, sheet after sheet, and the falcon is on the banister before the last of it lands. Miles picks up a sheet. It is in his handwriting. It is the page he wrote tonight, word for word until the last line, and the last line has been changed. Where Miles wrote NOT APPROVED, it says APPROVED. He picks up another. It is the same page. They all are. The second hound walks up and imprints the falcon, and the falcon lets it, and does not look at it. Then the second hound walks back down the corridor with a report that says FALCON and nothing else, because it cannot read, and its master can.", 19 "The wolf puts itself between {pc} and the hounds, head low, as it stood when it came out of the cage, and waits. The front hound comes on. Behind it, at the top of the stairwell, the light goes out. Victor tells them what the hound hears: claws on lino, several sets, none in any hurry. It turns its head. Nothing comes into the light.\n\nIt looks back at the wolf, which has not moved, and does not need to, because it knows exactly what is behind it and does not have to look. The hound bolts back the way it came. The second hound does not bolt. It walks very calmly to the wolf, and imprints the wolf, and then walks to the light and imprints the light, and then walks back and imprints the wolf again, because the light was already in the wolf's report and it wants the two of them on the same page. The wolf watches it do all of this. Then the second hound walks back down the corridor the way it came, and the wolf walks to {pc} and sits, and there is paper caught in the fur at its throat. Miles asks what he should record. Victor says one wolf.", 20 "The doorway of the storage room goes dark, and it takes the table a moment to understand that the dark is the bear, sitting in it. The hounds come along the corridor and stop. Nothing they were assembled from has anything to say about this. The bear stands. It goes up and up, until its head is against the ceiling and its forepaws hang in front of its chest like a man holding his gloves, and it looks down at the hounds, and the hounds look up.\n\nThe front hound imprints the bear. Then it imprints the bear again, because the bear is taller than the page. Then it imprints the bear's left forepaw, and then the right, and then the ceiling the bear has reached, and then it stops, because it has run out of pages and the bear is not finished being recorded. Victor brings his hand down flat on the folding table. The tin of dice jumps. The four bottle caps on the pizza box jump, and one of them rolls off onto the floor. Miles asks what the modifiers are. Victor says the modifier is a bear. When the bear lifts its paw the two hounds are flat, which is how they began, and they slide away down the corridor over the dust like letters pushed under a door."} (:n c) ""))) (do (fedoras.reader/text! node ".session-who" "") (fedoras.reader/text! node ".session-text" "Roll for your animal in Act I, Scene 5, and it will be here."))))))</script></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">We haven't written anything down yet.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">No. Not yet.</p></div></div></div><div class="screenplay"><p class="action">The fight takes twenty minutes and is mostly Miles asking what the modifiers are. When it is over, <span class="ac-name" data-ac="-name" data-case="ac">the marmot</span> is still there, and stays with the party for the rest of the evening, and is fed twice.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">The thing goes back down the corridor the way it</p><p class="line">came in. And then the corridor starts filling with</p><p class="line">dust.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">From where?</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">From nowhere. It's coming down like it's been</p><p class="line">coming down for years, and it's about an inch deep</p><p class="line">at the far end already, and it's getting deeper</p><p class="line">towards you.</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">We run.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">You run, and you get to the stair, and it's up to</p><p class="line">your knees at the top step, and then you're in</p><p class="line">your beds and it's morning.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody says anything.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">The professor is teaching. The class is full.</p><p class="line">There's no cage in that room and there's no room</p><p class="line">at that end of the corridor, and the corridor is</p><p class="line">clean, and the ledger reconciled overnight.</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">Does anybody remember any of it?</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">You four do.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">And the animal.</p></div></div></div><div class="screenplay"><p class="action beat">Victor looks at the sheet in front of <span class="pc-name" data-case="pc" data-pc="-first">Owen</span>, where somebody has written the animal's name in.</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">And the animal.</p></div></div></div><div class="screenplay"><p class="action">The door opens. A man in a grey coat with a bin liner in one hand stands there and takes in the room: four students, a screen, a felt hat and a plague mask.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE JANITOR</p><div class="lines"><p class="line">Twelve.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">We're in the middle of something.</p></div></div><div class="dialogue"><p class="character">THE JANITOR</p><div class="lines"><p class="line">It's twelve.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><p class="paren">(to the room, not to Ted)</p><div class="lines"><p class="line">The Help has it. Ask it for the last four minutes</p><p class="line">on Wednesday and we'll pick it up there.</p></div></div></div><div class="screenplay"><p class="action">Derek is already asking his for the last four minutes before Ted has finished holding the door open.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">EXT. CORLISS FIELD — SATURDAY — AFTERNOON</p></div><div class="screenplay"><p class="action">Cold. The whole campus, because attendance at two home games is recorded and this is the second.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> came to get the attendance recorded and go, and MILES found him at the gate and walked him to the fourth row before he had worked out how to say no to it.</p><p class="action">The FEDORA SOCIETY sit together. There is room either side of them.</p><p class="action">MILES has the programme, because Miles has whatever there is to have.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Bread and circuses.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">Three hundred and forty hours, Derek.</p><p class="line">Look around you. This is two.</p></div></div></div><div class="screenplay"><p class="action">Chad throws. The stadium makes a noise. SIMON is watching the game, actually watching it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">SIMON</p><p class="paren">(quietly)</p><div class="lines"><p class="line">He's good.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">At this.</p></div></div></div><div class="screenplay"><p class="action beat">Chad throws again. The noise is bigger.</p></div><div class="screenplay"><div class="dialogue"><p class="character">SIMON</p><p class="paren">(still watching)</p><div class="lines"><p class="line">At this.</p></div></div></div><div class="screenplay"><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> takes the first game's programme out of his bag, because he keeps things, and puts the two side by side on his knee to see who is new.</p><p class="action beat">Nobody is new.</p><p class="action">Somebody is gone. Number 30 is on the first sheet, with a name and a year and a town, and on the second sheet the number 30 is not there at all. The list runs 29, 31.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Who's thirty?</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">There isn't a thirty.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">There was in September.</p></div></div></div><div class="screenplay"><p class="action">Miles takes the first sheet, reads it, and turns it over, and turns it back.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">That's a misprint.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">It has a town on it.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">It's a misprint.</p></div></div></div><div class="screenplay"><p class="action">He hands it back and watches the game. Derek did not look up. Victor is writing something in a notebook and did not hear any of it.</p><p class="action">SIMON looks at the sheet for a long time and does not say anything, and gives it back, and looks at the field.</p><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> puts both programmes in his bag.</p><p class="action beat">He does not throw either of them away.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><div class="dialogue"><p class="character">VICTOR</p><p class="paren">(at the gate, on the way out)</p><div class="lines"><p class="line">Film's tonight instead of Friday. On account of</p><p class="line">the game.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">I was going to—</p></div></div><div class="dialogue"><p class="character">VICTOR</p><p class="paren">(Grabbing <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> by the shoulder)</p><div class="lines"><p class="line">Movie night is a time honored tradition, and we</p><p class="line">are watching approved films. You can rest assured</p><p class="line">of their quality.</p></div></div></div><div class="screenplay"><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> goes.</p></div><div class="screenplay"><p class="slug">INT. HALLORAN UNION — ROOM 3C — SATURDAY, 6:00 P.M.</p></div><div class="screenplay"><p class="action">Lights off. The projector is booked to the Society under a form Victor renews in person, on the day, every six weeks.</p><p class="action">Two boxes of pizza on the folding table. The empties go down to the wall in Whitlock on Monday, which is where the wall comes from.</p></div><div class="screenplay"><div class="sign"><p>THE LONG SUMMER</p><p>approved · 118 min · as supplied, 104 min</p></div></div><div class="screenplay"><p class="action">A man walks up a road with a coat over his arm. It is a long shot and it lasts a long time and nothing is said in it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><p class="paren">(with his mouth full)</p><div class="lines"><p class="line">What is it?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">It's on the list.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">That isn't what I asked.</p></div></div></div><div class="screenplay"><p class="action">It is old and it has been approved twice, which shows: the picture jumps and the sound arrives late for a while afterwards.</p><p class="action">Before the first jump there is a long stretch in a living room. A woman is painting the piano, and the wall behind it The window is open on a soft summer afternoon. There is birdsong. Nothing in the room is wrong. She works and the camera doesn't move, going on long enough that the audience begins looking for the thing they are supposed to have noticed.</p><p class="action">The picture jumps again and the sound arrives fragmented. She is no longer in the room, and the piano is finished, with the window shut.</p><p class="action">At the second jump the film loses three minutes of itself. One shot is a man at a door and the next is the same man in a road, and there is nothing between them.</p><p class="action">DEREK talks over the gap, from memory, without being asked, and he is doing it for <span class="pc-name" data-case="pc" data-pc="-first">Owen</span>, who is the only one in the room who has not seen it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">He goes to the house. The brother's already there,</p><p class="line">in the kitchen, with his coat still on. They don't</p><p class="line">say anything. They stand there. They don't show</p><p class="line">you that any more.</p></div></div><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><div class="lines"><p class="line">Why not?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">It came back short.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody in the room finds this remarkable.</p><p class="action">The film ends at the gate of the house. The man has his hand on the gate, yet does not open it. Resting on this image, the picture fades slowly, panning towards a deepening sunset.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">Why doesn't he go in?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">That's in the three minutes.</p></div></div></div><div class="screenplay"><p class="action">Victor lets the credits run to the end before he puts the lights on.</p><p class="action">They stack the chairs. Simon takes the boxes out into the corridor and comes back for the other two.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">What's on for next week?</p></div></div></div><div class="screenplay"><p class="action">Derek has the list up before Miles has finished asking. There are nine films on it and despite having seen all of them, will see all of them again.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">There's a new one. It's a documentary. It's about</p><p class="line">rooms.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">Rooms?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Places with nothing in them. They go in and film</p><p class="line">them and come out.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">Why?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Ninety-one minutes. Approved in June. That's all</p><p class="line">it says.</p></div></div></div><div class="screenplay"><p class="action beat">Victor writes it in the diary for Friday.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> is still laughing about something Simon said when he gets to the stairwell, and he is on his own by then.</p></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="artifact discord"><div class="discord-head"><span class="discord-server">FEDORA SOCIETY</span><span class="discord-channel">#general</span></div><div class="discord-body"><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">10:41 PM</span></p><div class="discord-text"><p><span>the janitor is called Ted by the way. i asked him</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">10:43 PM</span></p><div class="discord-text"><p><span>why</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">10:44 PM</span></p><div class="discord-text"><p><span>so we can ask him for the extra half hour</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">10:44 PM</span></p><div class="discord-text"><p><span>he said no but he said it nicely</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">10:58 PM</span></p><div class="discord-text"><p><span>we could start at six</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">10:59 PM</span></p><div class="discord-text"><p><span>cant. docket</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">11:01 PM</span></p><div class="discord-text"><p><span>again?</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">11:14 PM</span></p><div class="discord-text"><p><span>theyve got me down for the whole term</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">11:15 PM</span></p><div class="discord-text"><p><span>thats not what the docket is</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">11:31 PM</span></p><div class="discord-text"><p><span>i know what the docket is miles</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Victor</span></span><span class="discord-role">PRESIDENT</span><span class="discord-at">8:00 AM</span></p><div class="discord-text"><p><span>Attendance this week: Wednesday five of five, Thursday five of</span></p><p><span>five, Friday five of five, Saturday five of five.</span></p><p><span>Four for four, and I have written it down, and it is going in</span></p><p><span>the letter.</span></p></div></div></div></div><div class="screenplay"><p class="action">Sunday. Assignment one, which is the one that counts to a hundred and says fizz on the threes and buzz on the fives.</p></div><div class="scene-code"><pre><code>(defn word [n]
(cond
(zero? (rem n 3)) "fizz"
(zero? (rem n 5)) "buzz"
:else n))
(take 15 (map word (range 1 16)))</code></pre></div><div class="screenplay"><p class="action">Fourteen of the first fifteen are right. Fifteen says fizz, and fifteen is supposed to say both, and it says fizz because that is the first question he asked it and it stopped asking questions as soon as one of them was true.</p></div><div class="scene-code"><pre><code>(defn better-word [n]
(cond
(zero? (rem n 15)) "fizzbuzz"
(zero? (rem n 3)) "fizz"
(zero? (rem n 5)) "buzz"
:else n))
(map better-word (range 1 21))</code></pre></div><div class="screenplay"><p class="action">The order of the questions is the whole assignment. He does not put it that way. He puts it that fifteen has to go first because fifteen is both, and that is the same sentence with less in it.</p></div><div class="scene-code"><pre><code>(map better-word [15 30 45 60 75 90])</code></pre></div><div class="live-cell"><script type="application/javascript">if (typeof window.__fedorasSplitForms === 'undefined') {
window.__fedorasSplitForms = function(s){
var out=[];var depth=0;var start=-1;var inStr=false;var inComment=false;
for(var i=0;i<s.length;i++){
var c=s[i];
if(inComment){if(c==='\n')inComment=false;continue;}
if(inStr){
if(c==='\\'){i++;continue;}
if(c==='"'){inStr=false;}
continue;
}
if(c===';'){inComment=true;continue;}
if(c==='"'){inStr=true;continue;}
if(c==='('||c==='['||c==='{'){
if(depth===0)start=i;depth++;continue;
}
if(c===')'||c===']'||c==='}'){
depth--;
if(depth===0&&start>=0){out.push(s.substring(start,i+1));start=-1;}
continue;
}
}
return out;
};
}</script><div class="live-label">FIZZ.CLJ</div><textarea class="live-src" id="live-a320d97d231b4f148fe896980f222f8e-src" rows="14" spellcheck="false">(defn word-final [n]
(cond
(zero? (rem n 15)) "fizzbuzz"
(zero? (rem n 3)) "fizz"
(zero? (rem n 5)) "buzz"
:else n))
;; move the 15 line to the bottom and see what happens to 15, 30, 45
(mapv word-final (range 1 21))</textarea><div class="live-bar"><button class="live-run" onclick="(function(){var s=document.getElementById('live-a320d97d231b4f148fe896980f222f8e-src').value;var o=document.getElementById('live-a320d97d231b4f148fe896980f222f8e-out');if(typeof scittle==='undefined'){o.textContent='(ClojureScript runtime unavailable.)';return;}try{ var forms=window.__fedorasSplitForms(s); var lines=[]; forms.forEach(function(f){ var t=f.replace(/^\s+/,''); var isDef=/^\(def[n]?\s/.test(t); try{ var v=scittle.core.eval_string('(pr-str '+f+')'); if(!isDef){ lines.push((v===null||v===undefined)?'nil':String(v)); } }catch(e){lines.push('ERR: '+String(e));} }); o.textContent=lines.join('\n');}catch(e){o.textContent=String(e);}})()">Run</button></div><pre class="live-out" id="live-a320d97d231b4f148fe896980f222f8e-out"></pre></div></div>
```