---
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>
```
# Scene 1 — The Infirmary
```{=html}
<span style="display:none;"></span>
```
```{=html}
<blockquote class="handbook"><p class="rule-number">The Fedora Handbook, Rule #29</p><p class="rule-text">“”</p></blockquote>
```
```{=html}
<div class="scene"><div class="screenplay"><p class="slug">INT. CAMPUS INFIRMARY — WARD 2 — SUNDAY 23 NOVEMBER</p></div><div class="screenplay"><p class="action">Eight beds, three of them made up, one of them occupied. A window at the far end that does not open and a strip light that is off, which is the only kindness in the building.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> has been here since Saturday night. It is Sunday afternoon and he has been awake since about ten and has not been told very much.</p><p class="action">MAYA is in the chair beside the bed with a coat over her knees. She has been to her run twice and come back both times.</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">How long was it?</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Ninety seconds. I counted it, because the sheet</p><p class="line">says to.</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 sheet?</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">The one on the wall in the teaching lab that</p><p class="line">nobody reads. It says count it, don't hold them</p><p class="line">down, and don't put anything in their mouth.</p></div></div></div><div class="screenplay"><p class="action beat">She says all three of those the way you say a thing you have said to yourself several hundred times since Saturday.</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">Sorry.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Don't. That's the worst thing you could have said</p><p class="line">and you had a one in four chance.</p></div></div></div><div class="screenplay"><p class="action">There is a letter on the locker. It came at four in the morning and somebody printed it and left it there, and it was there before he was awake.</p></div><div class="screenplay"><p class="direction">The letter is below.</p></div><div class="artifact handout"><p class="handout-title">NEW CARTHAGE UNIVERSITY · HEALTH SERVICE · ASSESSMENT OUTCOME</p><p><span>SUBJECT: <span class="pc-name" data-case="pc" data-pc="-full">Owen Mercer</span> · PROVISIONAL 3812</span></p><p><span>EVENT: single episode, witnessed, resolved without intervention. Duration reported as approximately ninety seconds.</span></p><p><span>FINDINGS: within range.</span></p><p><span>CATEGORY: GEN-8.77x</span></p><p><span>SUBCLASS: x — recurrence not indicated on present evidence.</span></p><p><span>ADVICE: rest. Return if there is a further episode.</span></p><p><span>You may resume all normal activity, including recorded activity.</span></p><p><span>This outcome is not a diagnosis. Diagnosis is not a service provided by the Health Service.</span></p><p><span>Dining allocation is unaffected.</span></p></div><div class="screenplay"><p class="action">Findings within range. Category GEN-8.77x. Rest, and come back if it happens again. That is the clinical content of the piece of paper and by the middle of the afternoon he has it off by heart, including the x.</p></div><div class="artifact aichat"><p class="ai-model">HELP · personal · v9</p><div class="ai-body"><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>what is GEN-8.77x</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>GEN-8.77x is an assessment category in the general series.</span></p><p><span>The general series records single events of altered awareness</span></p><p><span>in an enrolled subject. The numeral indicates the schedule</span></p><p><span>under which the category was assigned. The subclass indicates</span></p><p><span>whether recurrence is indicated on present evidence.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>so x means it wont happen again</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>x indicates that recurrence is not indicated on present</span></p><p><span>evidence.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>thats not the same thing</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>No.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>how many categories are in the general series</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>Nineteen.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>what are they</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>Categories other than the assigned category are not disclosed</span></p><p><span>to the subject.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>ok but is 8.77 a bad one. like is it in the bad half or the fine half. im not asking you to tell me the others im asking where mine sits</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>Categories are not ranked.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>then why are they numbered</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>The numeral indicates the schedule.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>what schedule. nobody has asked me anything</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>The schedule is the instrument used to determine the</span></p><p><span>category.</span></p><p><span>Schedules are retained. Schedules are not released to the</span></p><p><span>subject.</span></p></div></div><div class="ai-turn ai-user"><p class="ai-role">USER</p><div class="ai-text"><p><span>so it was decided already</span></p></div></div><div class="ai-turn ai-assistant"><p class="ai-role">ASSISTANT</p><div class="ai-text"><p><span>Would you like me to book a follow-up?</span></p></div></div></div></div><div class="screenplay"><p class="action">Nineteen. It gives that one up in one word, having declined the question either side of it.</p><p class="action">He goes back over it twice. The answer he keeps returning to is the short one in the middle, because it is the only time all year that anything has agreed with him about anything.</p></div><div class="screenplay"><p class="slug">INT. WARD 2 — MONDAY 24 NOVEMBER</p></div><div class="screenplay"><p class="action">ELI arrives with a chair from another room because there is only one chair, and puts it down the wrong way round and sits on it like that.</p></div><div class="screenplay"><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">Right. I've been told not to tire you out, so I'm</p><p class="line">going to talk and you're going to lie there.</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">Fine.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">The Guild lost the black box for a fortnight</p><p class="line">because somebody in Music booked it on a form that</p><p class="line">doesn't exist any more, and I have spent four days</p><p class="line">on it and I have won, and nobody will ever know</p><p class="line">what it cost me.</p></div></div></div><div class="screenplay"><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> laughs, which hurts, which he had not expected.</p><p class="action">LENA comes in at some point in the middle of it with three coffees, sits on the end of the empty bed opposite, and does not say anything for a while, which is her version of asking.</p></div><div class="screenplay"><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">Have they told you what it was?</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">They've given me a letter.</p></div></div><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">Yes. They do that.</p></div></div></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. WARD 2 — MONDAY, 2:00 P.M.</p></div><div class="screenplay"><p class="action">A woman comes in with a tablet and a chair she has carried in herself, and sits down at the end of the bed rather than beside it, and does not give a name or a department.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">This is the schedule that goes with the category.</p><p class="line">Nine questions. There are no wrong answers, I'm</p><p class="line">not assessing you, I'm completing the schedule.</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've already got the category.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Yes.</p></div></div></div><div class="screenplay"><p class="action beat">She is not embarrassed by that and does not appear to think it is a remark.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Were you asleep?</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">No.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">In the ten minutes before, did you notice an odour</p><p class="line">you couldn't place?</p></div></div></div><div class="screenplay"><p class="action beat">A coin held too long, and then a room that has been shut.</p><p class="action beat">It has happened in a warm room in August and twice since and he has never once told anybody about the smell, because the smell is not the part other people can see.</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">No.</p></div></div></div><div class="screenplay"><p class="action">She types it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">In the last thirty days, have you been below</p><p class="line">ground level on institutional premises?</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">Yes. Plant Services. It's my service assignment.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">How long in total, would you say?</p></div></div></div><div class="screenplay"><p class="action beat">He works it out, which takes a moment.</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">Fifty. About fifty.</p></div></div></div><div class="screenplay"><p class="action">She types for longer than any yes or no has taken.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Do you recall a room, corridor or stair on</p><p class="line">institutional premises that you cannot place on a</p><p class="line">map?</p></div></div></div><div class="screenplay"><p class="action beat">There is a photograph of a catalogue card on his Help, in a bag, in the bench room, where it has been since Saturday.</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">No.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Has any family member been assessed under this</p><p class="line">schedule?</p></div></div></div><div class="screenplay"><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> looks at her.</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 don't have that.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Nobody does. It's on every schedule. It's older</p><p class="line">than the schedule. Say no.</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">No.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Was any person present who is not enrolled at this</p><p class="line">institution?</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">No. One person was there. She's enrolled.</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Name?</p></div></div></div><div class="screenplay"><p class="action beat">He gives it, because there is no reason not to, and watches her type it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">Last one. Since the event, have you wanted to know</p><p class="line">something you did not want to know before?</p></div></div></div><div class="screenplay"><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> looks at her.</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 sort of question is that?</p></div></div><div class="dialogue"><p class="character">THE ASSESSOR</p><div class="lines"><p class="line">It's question nine. There are no wrong answers.</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">No.</p></div></div></div><div class="screenplay"><p class="action">She types, stands, and takes the chair out with her.</p></div><div class="screenplay"><div class="dialogue"><p class="character">THE ASSESSOR</p><p class="paren">(at the door)</p><div class="lines"><p class="line">Thank you. That's the schedule.</p></div></div></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="artifact handout"><p class="handout-title">SCHEDULE 8 · nine questions · completed by an assessor · not a diagnostic instrument</p><p><span>1. Was the subject asleep?</span></p><p><span>2. In the ten minutes preceding, did the subject notice an odour they could not place?</span></p><p><span>3. In the preceding thirty days, has the subject been below ground level on institutional premises?</span></p><p><span>3a. If yes, total hours.</span></p><p><span>4. Does the subject recall a room, corridor or stair on institutional premises that they cannot place on a map?</span></p><p><span>5. Has any family member been assessed under this schedule?</span></p><p><span>6. Was any person present who is not enrolled at this institution?</span></p><p><span>7. Name of any person present.</span></p><p><span>8. (withdrawn)</span></p><p><span>9. Since the event, has the subject wanted to know something they did not want to know before?</span></p><p><span>Schedules are retained. Schedules are not released to the subject.</span></p></div><div class="screenplay"><p class="slug">INT. WARD 2 — MONDAY, LATER</p></div><div class="screenplay"><p class="action">LENA has brought the coffees again and is sitting on the end of the empty bed opposite with her feet up.</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">Somebody came and did a schedule.</p></div></div><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">Nine questions?</p></div></div></div><div class="screenplay"><p class="action beat"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> looks at her.</p></div><div class="screenplay"><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">They did four of them at me when I transferred.</p><p class="line">Not the sleeping one. The one about the corridor</p><p class="line">and the one about the smell and the one about</p><p class="line">family, and then the last one.</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">Question nine.</p></div></div><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">That's the one.</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">What did you say?</p></div></div><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">I said no, because I'd been asked it before and I</p><p class="line">knew what it was for.</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">What's it for?</p></div></div></div><div class="screenplay"><p class="action beat">She drinks the coffee.</p></div><div class="screenplay"><div class="dialogue"><p class="character">LENA</p><div class="lines"><p class="line">Nothing happens if you say no.</p></div></div></div><div class="screenplay"><p class="action">She changes the subject to the poster job and does not go back to it, and <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> does not push, and neither of them mentions it again for the rest of the book.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. WARD 2 — MONDAY EVENING</p></div><div class="screenplay"><p class="action">ELI has been here two hours and has run out of the Guild and most of the second years.</p><p class="action">He tears the back page off the notes at the end of the bed and draws four lines on it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">Noughts and crosses. You're crosses, you're ill.</p></div></div></div><div class="screenplay"><p class="action">They play nine games. Eli wins the first one because <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> is not really there yet, and after that they draw eight in a row.</p></div><div class="screenplay"><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">This is a terrible 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">It's a solved game.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">What does that mean?</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">If neither of us makes a mistake it's a draw.</p><p class="line">Every time. Forever.</p></div></div></div><div class="screenplay"><p class="action beat">Eli looks at the paper for a moment.</p></div><div class="screenplay"><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">So the whole of it is waiting for the other one to</p><p class="line">get it wrong.</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">Yes.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">God. All right. Best of one more.</p></div></div></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>Victor</span></span><span class="discord-role">PRESIDENT</span><span class="discord-at">Wednesday, 9:14 PM</span></p><div class="discord-text"><p><span>Attendance tonight four of five. The session does not proceed</span></p><p><span>at four. We hold the position and resume next week.</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">9:15 PM</span></p><div class="discord-text"><p><span>hes in the infirmary victor</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">9:16 PM</span></p><div class="discord-text"><p><span>I am aware of where he is. The position is held either way.</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">9:31 PM</span></p><div class="discord-text"><p><span>we could run the bit under the refectory without him</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">9:32 PM</span></p><div class="discord-text"><p><span>No.</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:02 PM</span></p><div class="discord-text"><p><span>has anyone actually gone over there</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:44 PM</span></p><div class="discord-text"><p><span>ive gone over there. theyll only let family in</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:45 PM</span></p><div class="discord-text"><p><span>i left the dice</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">11:58 PM</span></p><div class="discord-text"><p><span>Article Two stands. Nobody has left anything.</span></p></div></div></div></div><div class="screenplay"><p class="action">The tin is on the locker when he wakes up on Tuesday, with a note folded under it that says back Wednesday and nothing else, and Simon's handwriting is worse than anybody would have guessed.</p></div><div class="artifact email"><table class="email-head"><tbody><tr><td>From:</td><td><span>Office of the Registrar <no-reply@newcarthage.edu></span></td></tr><tr><td>To:</td><td><span><span class="pc-name" data-case="pc" data-pc="-full">Owen Mercer</span> <<span class="pc-name" data-case="pc" data-pc="-first">Owen</span>@newcarthage.edu></span></td></tr><tr><td>Date:</td><td>Tuesday, 4:00 AM</td></tr><tr><td>Subject:</td><td><strong><span>Absence — recorded</span></strong></td></tr></tbody></table><div class="email-body"><p><span>An absence of six days has been recorded. Supporting documentation has been received from the Health Service.</span></p><p><span>Recorded absence is not a component of review.</span></p><p><span>Coursework deadlines are not adjusted for recorded absence.</span></p><p><span>Set one closes Friday. Your outstanding supervised study sessions (2) remain outstanding.</span></p></div></div><div class="screenplay"><p class="action">Six days recorded, two evenings still owed, and set one due on the Friday the way it was due on the Friday before any of this.</p></div><div class="screenplay"><p class="slug">INT. WARD 2 — TUESDAY 25 NOVEMBER</p></div><div class="screenplay"><p class="action">He is dressed and sitting on the made bed with the letter in his hand, waiting for somebody to come and tell him he can go, which takes an hour and forty minutes.</p><p class="action">MAYA comes at ten with his bag, which has been in the bench room since Saturday, and the page folded into eight is still in the front pocket of it, and so is the card he photographed in Beckwith.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Anything?</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">GEN-8.77x.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Which means?</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">Findings within range. The x means recurrence</p><p class="line">isn't indicated on present evidence.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">That isn't the same as it not happening 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">No. I asked. It said no as well.</p></div></div></div><div class="screenplay"><p class="action beat">She reads the letter twice and gives it back.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Somebody wrote that schedule. Somebody set the</p><p class="line">range, on a day, and put a name at the bottom of</p><p class="line">it, and it went into a building and it is in that</p><p class="line">building now.</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 know.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">I'm not being nice to you. I've had this all term</p><p class="line">with a correction factor.</p></div></div></div><div class="screenplay"><p class="action">She has to go to the bench at half past and goes, and stops at the door.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">About Saturday.</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">Yes.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">I'm not going to say anything sensible about it in</p><p class="line">a ward. Come and find me when you're not grey.</p></div></div></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="screenplay"><p class="action">Tuesday. He is not allowed to do anything strenuous and the sliding puzzle is not going anywhere, so he writes the terrible game instead, because he has just told somebody it cannot be lost and he would like that to be true where he can check it.</p></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/empty-board
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/free?
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/place
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/lines
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/winner
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/full?
</code></pre></div>x<div class="screenplay"><p class="action">That is the whole of the rules and it took forty minutes, and it is the part he expected to be hard.</p><p class="action">Then the other half, which is a thing that plays. It works backwards from the end: if a board is finished it is worth one, nothing, or minus one, and if it is not finished it is worth whatever the best reply to it is worth, and the other side is looking for the worst.</p></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/other-side
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/outcome
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/value
</code></pre></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/best-move
</code></pre></div><div class="screenplay"><p class="action">It reads every game that is still possible from where it is standing, which is a lot of games at the start and almost none by the middle, which is why it thinks for a second on the first move and instantly after that.</p></div><div><pre><code class="sourceCode language-clojure printed-clojure">#'act3.s1-the-infirmary/opening
</code></pre></div>6<div class="screenplay"><p class="action">Six. It has to be six, and it is not blocking because it is clever, it is blocking because every board that does not block is worth minus one to it.</p></div>0<div class="screenplay"><p class="action">Nought. From an empty board, with both sides playing as well as they can play, the game is worth nought to everybody, which is what he told Eli on a piece of paper torn off the end of a bed.</p><p class="action">Play it. It blocks, it takes a win when there is one, and it will not lose to anybody on that corridor.</p></div><div><script type="application/x-scittle">(ns fedoras.sketch
"Editable ClojureScript that draws.
The live cells elsewhere in the book evaluate an expression and print
what it returned, which is fine for a list and useless for a picture.
A sketch cell gives the code a canvas and five things to draw with,
and then gets out of the way: what the reader edits is the same
algorithm that ran on the JVM at build time, in the same language,
and it runs in front of them.
What the page provides, and the scene says so out loud because Reyes
said it in class:
(clear!) wipe it
(ink! \"#1c1f24\") colour
(dot x y) one mark, in world coordinates
(poly [[x y] ...]) a closed outline
(frames! total per f) call (f i) total times, per of them a frame
Nothing else is injected. Everything above the fold in the cell is
the reader's.
WHERE THE CODE IS. All of it is here, in one file read twice. The
build reads the :clj half, which writes the markup and, for each cell,
a header binding the short names to that cell's own canvas or piece of
page. The page reads the :cljs half, because the build ships this file
to it as it stands: the drawing kit the headers call, and the runner
behind every Run button."
#?(:clj (:require [clojure.string :as str]
[fedoras.state :as state]
[scicloj.kindly.v4.kind :as kind])))
;; ---------------------------------------------------------------------
;; The build: headers and markup
;; ---------------------------------------------------------------------
#?(:clj
(do
(defn canvas-header
"The short names a sketch cell may use, bound to its own canvas.
Built as Clojure forms rather than as a string, so it cannot be
unbalanced and can be read back and printed."
[id world-w world-h]
[(list 'def 'C (list 'fedoras.sketch/mount id world-w world-h))
'(def cv (:cv C))
'(def ctx (:ctx C))
'(def CW (:cw C))
'(def CH (:ch C))
'(def SX (:sx C))
'(def SY (:sy C))
'(defn clear! [] (fedoras.sketch/clear! C))
'(defn ink! [c] (fedoras.sketch/ink! C c))
'(defn dot ([x y] (fedoras.sketch/dot C x y)) ([x y s] (fedoras.sketch/dot C x y s)))
'(defn poly [pts] (fedoras.sketch/poly C pts))
'(defn frames! [total per f] (fedoras.sketch/frames! C total per f))
'(defn on-pointer! [f] (fedoras.sketch/on-pointer! C f))
'(def page-ink (fedoras.sketch/page-ink C))
'(clear!)
'(ink! page-ink)
'(fedoras.sketch/line-width! C 0.8)])
(defn page-header
"The names a page cell may use. No canvas: a piece of the page."
[id]
[(list 'def 'root (list '.getElementById 'js/document (str id "-out")))
'(defn html! [s] (set! (.-innerHTML root) s))
'(defn on! [id f]
(let [e (.getElementById js/document id)]
(when e (set! (.-onclick e) (fn [_] (f))))))
'(defn fetch [k]
(try
(let [v (.getItem js/localStorage (str "fedoras." k))]
(when v (read-string v)))
(catch :default _ nil)))
'(defn store! [k v]
(try (.setItem js/localStorage (str "fedoras." k) (pr-str v))
(catch :default _ nil))
v)])
(defn- header-text [forms]
(str/join "\n" (map pr-str (cons '(in-ns 'fedoras.sketch-runner) forms))))
(defn primitives
"What the page hands a cell, printed, so that nothing in the book is
doing something the reader cannot look at."
([] (primitives 100 100))
([world-w world-h]
(kind/code (header-text (canvas-header "the-page" world-w world-h)))))
(defn cell
"An editable ClojureScript sketch with a canvas under it.
(sketch/cell {:id \"chaos\" :label \"CHAOS.CLJS\" :world [100 86.6]}
\"(dot 50 40)\")"
[{:keys [id label world w h rows]
:or {id "sketch" world [100 100] w 420 h 380 rows 16}}
code]
(let [[ww wh] world]
(kind/hiccup
[:div
(state/source "fedoras/sketch.cljc")
[:div.sketch {:id id}
[:div.roll-head
[:span.roll-label (or label "SKETCH")]
[:span.roll-dc (str ww " × " wh)]]
[:textarea {:id (str id "-src") :class "live-src" :rows rows :spellcheck "false"}
(str/trim code)]
[:div.live-bar
[:button.live-run "Run"]]
[:canvas {:id (str id "-canvas") :width w :height h}]
[:pre {:id (str id "-out") :class "live-out"}]]
(state/call 'fedoras.sketch/cell! id (header-text (canvas-header id ww wh)))])))
(defn page
"An editable ClojureScript cell with a piece of page under it instead
of a canvas. The header gives the code five things:
root the div it owns
(html! s) put markup in it
(on! id f) wire a click
(fetch k) read something the reader saved
(store! k v) save something, and it is still there tomorrow
Everything else in the cell belongs to the reader."
[{:keys [id label rows] :or {id "page" rows 20}} code]
(kind/hiccup
[:div
(state/source "fedoras/sketch.cljc")
[:div.sketch {:id id}
[:div.roll-head
[:span.roll-label (or label "PAGE")]
[:span.roll-dc "cljs"]]
[:textarea {:id (str id "-src") :class "live-src" :rows rows :spellcheck "false"}
(str/trim code)]
[:div.live-bar
[:button.live-run "Run"]]
[:div {:id (str id "-out") :class "sketch-page"}]
[:pre {:id (str id "-res") :class "live-out"}]]
(state/call 'fedoras.sketch/cell! id (header-text (page-header id)))]))))
;; ---------------------------------------------------------------------
;; The page: the drawing kit and the runner
;; ---------------------------------------------------------------------
#?(:cljs
(do
(defonce counters
(atom {}))
(defn- counters-for
"The frame counters of one canvas: made the first time it is mounted
and kept for every Run after, so that each press of Run takes the
next number and the loop the last press started sees it and stops."
[id]
(or (get @counters id)
(let [made {:run (atom 0) :i (atom 0)}]
(swap! counters assoc id made)
made)))
(defn mount
"Everything one cell needs, from the id of its canvas and the size of
the world it draws in. Every drawing function below takes the map
this returns."
[id world-w world-h]
(let [cv (.getElementById js/document (str id "-canvas"))
ctx (.getContext cv "2d")
cw (.-width cv)
ch (.-height cv)]
(merge {:cv cv :ctx ctx :cw cw :ch ch
:sx (/ cw world-w)
:sy (/ ch world-h)}
(counters-for id))))
(defn ink!
"Both fill and stroke, because a cell that sets one and not the other
is a cell that draws in two colours by accident."
[{:keys [ctx]} colour]
(set! (.-fillStyle ctx) colour)
(set! (.-strokeStyle ctx) colour))
(defn page-ink
"Whatever colour the surrounding prose is, so a drawing belongs to the
page it is on and follows it into dark mode."
[{:keys [cv]}]
(.-color (js/getComputedStyle cv)))
(defn clear! [{:keys [ctx cw ch]}]
(.clearRect ctx 0 0 cw ch))
(defn dot
([c x y] (dot c x y 1.2))
([{:keys [ctx sx sy ch]} x y size]
(.fillRect ctx (* x sx) (- ch (* y sy)) size size)))
(defn poly
"A closed outline through the points, in world coordinates."
[{:keys [ctx sx sy ch]} pts]
(when (seq pts)
(let [[fx fy] (first pts)]
(.beginPath ctx)
(.moveTo ctx (* fx sx) (- ch (* fy sy)))
(doseq [[x y] (rest pts)]
(.lineTo ctx (* x sx) (- ch (* y sy))))
(.closePath ctx)
(.stroke ctx))))
(defn line-width! [{:keys [ctx]} w]
(set! (.-lineWidth ctx) w))
(defn- tick! [{:keys [run i] :as c} generation total per f]
(when (= generation @run)
(dotimes [_ per]
(when (< @i total) (f @i) (swap! i inc)))
(when (< @i total)
(js/requestAnimationFrame (fn [] (tick! c generation total per f)))))
nil)
(defn frames!
"Call (f i) total times, per of them per frame.
Pressing Run again retires the loop that is already going. Without
that, a cell run twice advances twice per frame and looks broken in a
way nobody can debug from the outside."
[{:keys [run i] :as c} total per f]
(let [generation (swap! run inc)]
(reset! i 0)
(tick! c generation total per f))
nil)
(defn on-pointer!
"Pointer position in world coordinates, which is what a cell wants and
never what the event gives it."
[{:keys [cv sx sy]} f]
(set! (.-onmousemove cv)
(fn [e]
(f (/ (.-offsetX e) sx)
(- (/ (.-height cv) sy) (/ (.-offsetY e) sy))))))
(defonce headers
(atom {}))
(defn- element
[id]
(.getElementById js/document id))
(defn- line-count
[text]
(count (re-seq #"\n" text)))
(defn- error-text
"An error as the reader should see it: the message, and the line of
their own code it came from, when there is one."
[error header-lines]
(let [line (:line (ex-data error))]
(str (or (ex-message error) (str error))
(when (and line (> line header-lines))
(str " (line " (- line header-lines) ")")))))
(defn run-cell!
"Cell `id`'s header, then the reader's code, with what came back
printed under the cell, or the error, counted in the reader's lines."
[id]
(let [header (get @headers id)
out (or (element (str id "-res")) (element (str id "-out")))
source (.-value (element (str id "-src")))]
(set! (.-textContent out)
(try
;; load-string is Scittle's own; plain ClojureScript has none
(let [value #_{:clj-kondo/ignore [:unresolved-symbol]} (load-string (str header "\n" source))]
(if (nil? value) "" (str value)))
(catch :default error
(error-text error (inc (line-count header))))))))
(defn- prepare-canvas!
"Give a sketch cell's canvas its drawing context while the page
loads, so that the first Run draws on a canvas the browser has
already shown once. A context made at the moment of the first
drawing loses that drawing in Firefox: the first frames of the
first Run never appear."
[id]
(when-let [cv (element (str id "-canvas"))]
(.clearRect (.getContext cv "2d") 0 0 (.-width cv) (.-height cv))))
(defn cell!
"Keep cell `id`'s header and wire its Run button, and the first time
the cell is seen, prepare its canvas, if it has one. Only the first
time, because preparing clears, and a canvas that is already drawing
must not be cleared under it."
[id header]
(let [first-time? (not (contains? @headers id))]
(swap! headers assoc id header)
(when-let [button (some-> (element id) (.querySelector ".live-run"))]
(set! (.-onclick button) (fn [_] (run-cell! id))))
(when first-time?
(prepare-canvas! id))))))</script><div class="sketch" id="noughts"><div class="roll-head"><span class="roll-label">NOUGHTS.CLJS</span><span class="roll-dc">cljs</span></div><textarea class="live-src" id="noughts-src" rows="30" spellcheck="false">(def board (atom (vec (repeat 9 nil))))
(def lines [[0 1 2] [3 4 5] [6 7 8]
[0 3 6] [1 4 7] [2 5 8]
[0 4 8] [2 4 6]])
(defn winner [b]
(some (fn [l]
(let [v (nth b (first l))]
(when (and v (apply = (map (fn [i] (nth b i)) l))) v)))
lines))
(defn free-squares [b]
(filter (fn [i] (nil? (nth b i))) (range 9)))
(defn wins-for [b mark]
(first (filter (fn [i] (= mark (winner (assoc b i mark)))) (free-squares b))))
(defn reply [b]
(or (wins-for b :o) ;; take the win
(wins-for b :x) ;; else stop theirs
(first (filter (fn [i] (some #{i} (free-squares b))) [4 0 2 6 8 1 3 5 7]))))
(defn show []
(html!
(str "<div style='display:grid;grid-template-columns:repeat(3,3rem);gap:4px'>"
(apply str
(map (fn [i]
(let [v (nth @board i)]
(str "<button id='c" i "' style='height:3rem'>"
(case v :x "X" :o "O" " ") "</button>")))
(range 9)))
"</div><p>"
(let [w (winner @board)]
(cond (= w :x) "you win. that should not happen."
(= w :o) "it wins."
(empty? (free-squares @board)) "drawn, as advertised."
:else "your move. you are crosses."))
"</p>"))
(doseq [i (range 9)]
(on! (str "c" i)
(fn []
(when (and (nil? (nth @board i)) (nil? (winner @board)))
(swap! board assoc i :x)
(when (and (nil? (winner @board)) (seq (free-squares @board)))
(swap! board assoc (reply @board) :o))
(show))))))
(show)</textarea><div class="live-bar"><button class="live-run">Run</button></div><div class="sketch-page" id="noughts-out"></div><pre class="live-out" id="noughts-res"></pre></div><script type="application/x-scittle">(if-let [mount (resolve (quote fedoras.sketch/cell!))] (mount "noughts" "(in-ns (quote fedoras.sketch-runner))\n(def root (.getElementById js/document \"noughts-out\"))\n(defn html! [s] (set! (.-innerHTML root) s))\n(defn on! [id f] (let [e (.getElementById js/document id)] (when e (set! (.-onclick e) (fn [_] (f))))))\n(defn fetch [k] (try (let [v (.getItem js/localStorage (str \"fedoras.\" k))] (when v (read-string v))) (catch :default _ nil)))\n(defn store! [k v] (try (.setItem js/localStorage (str \"fedoras.\" k) (pr-str v)) (catch :default _ nil)) v)") (js/console.error "fedoras: fedoras.sketch/cell! 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"><p class="action">Then three weeks, which go the way weeks go.</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>Victor</span></span><span class="discord-role">PRESIDENT</span><span class="discord-at">26 Nov, 11:40 PM</span></p><div class="discord-text"><p><span>Attendance four of five. The session holds. It resumes when it is five.</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">26 Nov, 11:52 PM</span></p><div class="discord-text"><p><span>hes back tomorrow</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">3 Dec, 11:38 PM</span></p><div class="discord-text"><p><span>Five of five.</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 Dec, 11:20 PM</span></p><div class="discord-text"><p><span>friday is the one with the boats again. thats twice this term</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 Dec, 11:21 PM</span></p><div class="discord-text"><p><span>nine films</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Miles</span></span><span class="discord-at">12 Dec, 6:02 PM</span></p><div class="discord-text"><p><span>review week. good luck everyone. genuinely</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">12 Dec, 6:40 PM</span></p><div class="discord-text"><p><span>good luck</span></p></div></div></div></div><div class="screenplay"><p class="action">Set one goes in on the twelfth of December, six of six, and the last of the six is the terrible game, which is marked as an assignment about a rule that cannot be broken, which it is.</p><p class="action">Review is the week after. Nobody works. Everybody works. The reading room is full at four in the morning for six nights and the Commons never closes, and there is a fortnight of the year in which this institution genuinely resembles the thing on the banner.</p></div><div class="screenplay"><p class="slug">INT. HALLORAN UNION — ROOM 3C — FRIDAY 17 APRIL</p></div><div class="screenplay"><div class="sign"><p>FOUR THOUSAND AND SIX</p><p>approved · 138 min · as supplied, 138 min</p></div></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Whole.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">They're always whole. Have you noticed that these</p><p class="line">are always whole?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">No.</p></div></div></div><div class="screenplay"><p class="action">It opens on a card and the card is a number and the number is the title.</p></div><div class="screenplay"><div class="sign"><p>FOUR THOUSAND AND SIX APPROVED ACTIVITIES</p><p>AND THE NUMBER HAS NEVER GONE DOWN</p></div></div><div class="screenplay"><p class="action">Then a settlement. It is not named and it is not placed and the countryside around it is not any countryside.</p><p class="action">The people in it are gathering. That is the offence and the film is clear about it and says it four times: they have been gathering, and the gathering is not on the list, and it has been going on for two years.</p><p class="action beat">The film never says what they gather about.</p><p class="action">It cannot. A film that reproduced the content would have to have the content approved and the content is the reason there is a film.</p><p class="action">So they talk, at length, in three long scenes, and they are shot beautifully, and there are no words on the screen and there is no dubbing and the sound of the room is left in.</p><p class="action">You watch a hundred people make an argument for nine minutes and you do not learn one thing about it.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">THE FILM — THE HALL — NIGHT</p></div><div class="screenplay"><p class="action">They sing.</p><p class="action beat">The audio is not there.</p><p class="action">It is not muted and it is not covered with score. It is removed, and the room tone goes with it, so the picture runs on and there is nothing coming out of the wall for about forty seconds while a hundred people sing.</p><p class="action">Their mouths go on. Some of them have their eyes shut.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><p class="paren">(in the dark, quietly)</p><div class="lines"><p class="line">Did the sound go?</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">No.</p></div></div></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">THE FILM — THE ASSIGNMENT — CONTINUOUS</p></div><div class="screenplay"><p class="action">It is an assignment. It is not a war, because war is unapproved and has been since the war, and the film uses the correct word about eleven times.</p><p class="action">The unit is nine people and they are extremely likeable and one of them is funny in a way Derek repeats twice afterwards.</p><p class="action">The clearance takes twenty-six minutes of screen time.</p><p class="action">It is well made. The camera stays where the unit is. It does not linger and it does not look away and it is excited, and the score under it is the best thing in the film.</p><p class="action">There is a corridor with two doors on it and the second door takes four seconds longer than the first, and the film makes those four seconds thrilling, and they are.</p><p class="action">Afterwards the unit tidies.</p><p class="action">The floor of the hall is hosed. There is a vehicle at the door and the loading is done properly and quickly by people who have done it before, and the film shows the whole of it because the whole of it is the point: this is a job, and these are the people who do it, and they are tired and they are decent to each other.</p><p class="action beat">One of them is sick behind the vehicle and another one carries his kit for him and neither of them says anything about it.</p><p class="action">That is the best scene in the film and everybody in Room 3C knows it.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="action">There is a shot in the transport on the way back of the unit in rows, in webbing, in the amber light they use in a hold, and one of them is asleep with his head against the wall.</p><p class="action">Rows, and amber, and being held.</p><p class="action">He has about four things from the first eighteen months and he has never told anybody any of them, because everybody has them and there is nothing to say.</p><p class="action">Warm. Amber, through a wall you could see through and could not see out of.</p><p class="action">There was another one, close, and it moved when he moved, and it was a long time before he understood that this was because it was the same distance from him as he was from it, and not because it was him.</p><p class="action">Things arrived. Not from anybody and not in a voice. The word for a chair, and then the word for a door, and then the words for how many there are of a thing. He did not learn them, so much had been given them.</p><p class="action">And hands, sometimes in gloves, from outside the wall, turning him so that something could be put against him. It was cold and it was not unkind and it happened on a schedule and he could tell when it was coming.</p><p class="action">And the taste.</p><p class="action">It changed twice that he can remember. Once it got better and stayed better for a long time, and once it changed and did not get better, and he has no idea how long either of those was because there was no outside and no day.</p><p class="action">The Commons does the hot line from half six. He has been through it four hundred times this year and he has never once stood in that queue and thought about the sac.</p><p class="action">He thinks about it now, in a dark room, over a shot of nine people in a hold, and then the film cuts to something else and so does he.</p></div><div class="screenplay"><p class="slug">THE FILM — THE END</p></div><div class="screenplay"><p class="action">The unit goes home. There is a woman at a gate and a child and it is not overdone.</p><p class="action">Then the closing card.</p></div><div class="screenplay"><div class="sign"><p>FOUR THOUSAND AND ELEVEN APPROVED ACTIVITIES</p><p>AND THE NUMBER HAS NEVER GONE DOWN</p></div></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. ROOM 3C — CONTINUOUS</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">That's the best thing we've watched all year and</p><p class="line">I'm not going to be talked out of it.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">It's very well made. It is extremely well made.</p><p class="line">The corridor is four seconds and they are the four</p><p class="line">best seconds on that list.</p></div></div></div><div class="screenplay"><p class="action">Miles has not said anything since the card.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">It's not the same number.</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">What isn't?</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">The front card and the back card. It opens on four</p><p class="line">thousand and six and it closes on four thousand</p><p class="line">and eleven.</p></div></div><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">So it went up.</p></div></div><div class="dialogue"><p class="character">MILES</p><div class="lines"><p class="line">It's the same film. It's a hundred and</p><p class="line">thirty-eight minutes. It went up five during the</p><p class="line">film.</p></div></div></div><div class="screenplay"><p class="action beat">Derek thinks about it.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">Then five things got approved while we were</p><p class="line">watching it. That's good, Miles.</p></div></div></div><div class="screenplay"><p class="action">Miles writes it down. He puts it in the column he ruled in October, which is HEARD, and it is the only thing in that column that came off a screen.</p></div><div class="screenplay"><div class="dialogue"><p class="character">SIMON</p><p class="paren">(quietly)</p><div class="lines"><p class="line">They sang for forty seconds.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">They did.</p></div></div><div class="dialogue"><p class="character">SIMON</p><div class="lines"><p class="line">No — I mean somebody sat down and took it out.</p><p class="line">Somebody had it and took it out and put nothing in</p><p class="line">the hole. That's a job. Somebody has that job.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody has anything for that.</p></div><div class="screenplay"><div class="dialogue"><p class="character">DEREK</p><div class="lines"><p class="line">The corridor, though.</p></div></div><div class="dialogue"><p class="character">VICTOR</p><div class="lines"><p class="line">The corridor was very good.</p></div></div></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">EXT. THE QUAD — 22 DECEMBER</p></div><div class="screenplay"><p class="action">The campus thins in about four hours. <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> watches a family load a car and it takes them nine minutes and they are laughing the whole time.</p><p class="action">MAYA and LENA are going. ELI is going and has said goodbye four times and come back twice.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Are you going anywhere?</p></div></div></div><div class="screenplay"><p class="action beat">He says nothing for slightly too long.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Right.</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 service assignment doesn't stop.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">The service assignment is two mornings a week and</p><p class="line">you know that isn't why.</p></div></div></div><div class="screenplay"><p class="action beat">She goes. At the gate she turns round and says something he does not hear and does not ask her to repeat, and it is the last thing anybody says to him for six days.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. MERIDIAN HALL — REYES 204 — 29 DECEMBER</p></div><div class="screenplay"><p class="action">The building is locked and the door of 204 is open because she is in it, and she is the only person on that floor.</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">Professor?</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Term's over. Come back the eighth.</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 wanted to ask what you did. To get here.</p></div></div></div><div class="screenplay"><p class="action">She looks up and decides how much of her afternoon this is going to be.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Provisional?</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">Yes.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">What did you fix?</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 water on my block.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">How?</p></div></div></div><div class="screenplay"><p class="action beat">He does not have an answer to this and never has.</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 hit it.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">I was a Provisional. It isn't a story. There were</p><p class="line">four of us in my year and I'm the one who's still</p><p class="line">here.</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">What happened to the others?</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">They stopped being here.</p></div></div></div><div class="screenplay"><p class="action beat">She goes back to the screen.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Don't come to me for this. I've got thirty-one of</p><p class="line">you and I like about four, and liking you is not a</p><p class="line">service the university provides.</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">Okay.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">The eighth. Bring the assignment.</p></div></div></div><div class="screenplay"><p class="action">He is at the door when she speaks again, without turning round.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">You'll be fine. Statistically most of you are.</p></div></div></div><div class="screenplay"><p class="transition">Blackout.</p></div></div>
```