---
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 4 — The First Week
```{=html}
<span style="display:none;"></span>
```
```{=html}
<blockquote class="handbook"><p class="rule-number">The Fedora Handbook, Rule #9</p><p class="rule-text">“Every institution opposes you. This is how you know it is an institution.”</p></blockquote>
```
```{=html}
<div class="scene"><div class="screenplay"><p class="slug">INT. MERIDIAN HALL — CS 101 — MONDAY, 8:00 A.M.</p></div><div class="screenplay"><p class="action">Forty desks. Thirty-one freshmen and one senior.</p><p class="action">PROFESSOR AMELIA REYES writes four words on the board.</p></div><div class="screenplay"><div class="sign"><p>PUT THE HELPS AWAY</p></div></div><div class="screenplay"><p class="action beat">Nobody moves.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">I'll wait.</p></div></div></div><div class="screenplay"><p class="action">They put the Helps away. A girl in the second row keeps hers in her lap until Reyes looks at her.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Who here has written a program.</p></div></div></div><div class="screenplay"><p class="action">Two hands. <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> puts his up as far as his shoulder and leaves it there.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">You. Half a hand.</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 did one on Friday. It says my name.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Did you write it?</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 Help wrote it.</p><p class="line">It didn't run. I fixed it.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">How?</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 moved one of the brackets.</p></div></div></div><div class="screenplay"><p class="action beat">Reyes looks at him for slightly longer than she has looked at anybody else this morning.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Then you're a week ahead. Don't get comfortable.</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 four words.</p></div></div><div class="dialogue"><p class="character">REYES</p><p class="paren">(shrugs)</p><div class="lines"><p class="line">It's four words that ran.</p><p class="line">Second question.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Who here is comfortable with their Help.</p></div></div></div><div class="screenplay"><p class="action beat">Every hand in the room.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Yes.</p></div></div></div><div class="screenplay"><p class="action">She turns the board on. It is not a Help. It is a screen with a page on it and the page does what she types.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Six programs, in three weeks from this morning, in</p><p class="line">any order you like, and between them they're a</p><p class="line">third of the term. I'll show you three of them</p><p class="line">now. I'll show you the other three next Monday, by</p><p class="line">which time you won't have finished these, and</p><p class="line">that's fine. That's what the three weeks are for.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">One.</p></div></div></div><div class="screenplay"><p class="action">Numbers run down the page. Some of them are words.</p></div><div class="screenplay"><div class="sign"><p>1 2 FIZZ 4 BUZZ FIZZ 7 8 FIZZ BUZZ 11 FIZZ</p><p>…</p><p>14 FIZZBUZZ 16</p></div></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Every third one says fizz. Every fifth says buzz.</p><p class="line">You can work out the rest.</p></div></div><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">What's it for?</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Nothing. It's for me to see whether you can be</p><p class="line">told a rule.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Two.</p></div></div></div><div class="screenplay"><p class="action">A word appears as dashes. She types letters at it. Wrong ones stack up on the right, under a heading everybody in the room can already recite.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Six wrong and it stops. If it lets you have seven,</p><p class="line">that's marked. If it takes a letter twice, that's</p><p class="line">marked.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Three.</p></div></div></div><div class="screenplay"><p class="action">A list. She adds a thing to it, finishes a thing on it, removes a thing from it. Then she closes the page, opens it again, and the list is still there.</p><p class="action beat">Somebody says 'oh.'</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">That last part is the assignment. Anyone can make</p><p class="line">a list. It has to still be there on Tuesday.</p></div></div><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">Can we use our Helps?</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Not on these six. Not on the next six. Not on the</p><p class="line">last six.</p></div></div><div class="dialogue"><p class="character">ANOTHER STUDENT</p><div class="lines"><p class="line">On what, then?</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Everything else. Use it constantly.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody writes that down. It is the only thing she says that morning that anybody repeats afterwards.</p></div><div class="screenplay"><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">Why can't we, though.</p><p class="line">For these.</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Because sooner or later something you need will be</p><p class="line">shut, and there will have to be somebody in the</p><p class="line">room who can open it.</p></div></div></div><div class="screenplay"><p class="action beat">Thirty-one people look at her.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">It won't be most of you, and that's fine. Most of</p><p class="line">you will go and do something else and be perfectly</p><p class="line">happy, and when a thing is shut you'll wait, and</p><p class="line">somebody will come, and one day nobody will. I</p><p class="line">teach it every year in case two of you are</p><p class="line">listening.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody asks anything else.</p></div><div class="screenplay"><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Three.</p></div></div><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">What if we get stuck?</p></div></div><div class="dialogue"><p class="character">REYES</p><div class="lines"><p class="line">Then you're stuck.</p></div></div></div><div class="screenplay"><p class="action">VICTOR is in the third row with a notebook, a pen, and a second pen. He is the only person in the room who is not eighteen.</p><p class="action beat">Reyes starts the class.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="artifact handout"><p class="handout-title">CS 101 · Assignment Set One</p><p><span>Six programs. Three weeks. Any order. One third of the term.</span></p><table class="handout-table"><tbody><tr><td class="k">1 · FIZZBUZZ</td><td class="v"><span>Count to a hundred. Every third one says fizz, every fifth says buzz. Both, both.</span></td></tr><tr><td class="k">2 · NOTICES</td><td class="v"><span>Guess a word. Six wrong and you're out. Six means six, and a letter twice is not a guess.</span></td></tr><tr><td class="k">3 · A LIST OF THINGS TO DO</td><td class="v"><span>Add, finish, remove. It has to still be there on Tuesday.</span></td></tr><tr><td class="k">4 · CHAOS DOTS</td><td class="v"><span>Ten thousand dots placed by one rule. I will not tell you the rule and I will know if you copied the shape.</span></td></tr><tr><td class="k">5 · THE SLIDING PUZZLE</td><td class="v"><span>Fifteen numbers and a hole. It must refuse illegal moves.</span></td></tr><tr><td class="k">6 · SQUARES</td><td class="v"><span>A square inside a square inside a square. Extra if it turns.</span></td></tr></tbody></table><p><span>If it draws, it draws in a page. If it crashes in front of me, it did not run.</span></p><p><span>No Helps on these six. I will know.</span></p><p><span>On everything that is not these six, use your Help. That is what it is for.</span></p></div><div class="screenplay"><p class="slug">INT. PRENTISS SCIENCE CENTER — TEACHING LAB — TUESDAY</p></div><div class="screenplay"><p class="action">Thirty-nine stools. By the door, a bin of goggles. Most of them are scratched to a fog.</p><p class="action">PROFESSOR EVELYN CARTER goes round the room once and has every name.</p></div><div class="screenplay"><div class="dialogue"><p class="character">CARTER</p><div class="lines"><p class="line">Partners are alphabetical. I don't take requests.</p><p class="line">Before we start, we have to watch this.</p></div></div></div><div class="screenplay"><p class="action">A screen comes down over the periodic table.</p></div><div class="screenplay"><div class="sign"><p>MERIDIAN SYSTEMS</p><p>AN INTRODUCTION TO CHEMISTRY</p><p>approved · 4 min</p></div></div><div class="screenplay"><div class="dialogue"><p class="character">NARRATOR (O.S.)</p><div class="lines"><p class="line">Chemistry is the study of what things are made of,</p><p class="line">and of how they can be improved.</p></div></div></div><div class="screenplay"><p class="action">A hand in a glove picks up a beaker. The liquid in it is blue. Nothing in the room is blue.</p></div><div class="screenplay"><div class="dialogue"><p class="character">NARRATOR (O.S.)</p><div class="lines"><p class="line">Every chemical you will handle in this course is</p><p class="line">an approved chemical. Approved chemicals are safe,</p><p class="line">understood, and sufficient.</p></div></div><div class="dialogue"><p class="character">NARRATOR (O.S.)</p><div class="lines"><p class="line">The creation of novel compounds is a regulated</p><p class="line">activity and is beyond the scope of this course.</p></div></div></div><div class="screenplay"><p class="action">A diagram. Six shapes join to make a seventh. The seventh is not shown for long.</p></div><div class="screenplay"><div class="dialogue"><p class="character">NARRATOR (O.S.)</p><div class="lines"><p class="line">If a reaction does not proceed as described, stop,</p><p class="line">and tell your instructor.</p></div></div></div><div class="screenplay"><p class="action beat">A pause of about a second.</p></div><div class="screenplay"><div class="dialogue"><p class="character">NARRATOR (O.S.)</p><div class="lines"><p class="line">If a reaction proceeds as described and you did</p><p class="line">not perform it, stop, and tell your instructor.</p></div></div></div><div class="screenplay"><p class="action">Nobody in the room moves. Two people are on their Helps.</p></div><div class="screenplay"><div class="dialogue"><p class="character">NARRATOR (O.S.)</p><div class="lines"><p class="line">Chemistry is safe.</p></div></div></div><div class="screenplay"><div class="sign"><p>MERIDIAN SYSTEMS</p><p>keeping the ordinary, expected</p></div></div><div class="screenplay"><p class="action">Carter watches it with them, standing at the side with her arms folded. She has seen it. She watches it anyway, all four minutes, and when it finishes she waits for the screen to go up before she says anything.</p></div><div class="screenplay"><div class="dialogue"><p class="character">CARTER</p><div class="lines"><p class="line">Right.</p></div></div></div><div class="screenplay"><p class="action">MAYA FLORES sits down next to <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> and hands him a pair of goggles without looking at him.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Those are the ones that don't fog.</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">How do you know?</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">I came Sunday and tried all of them.</p></div></div></div><div class="screenplay"><p class="action">They heat a solution until it goes from clear to green. Thirty-nine of them go green.</p></div><div class="screenplay"><div class="dialogue"><p class="character">MAYA</p><p class="paren">(reading the sheet)</p><div class="lines"><p class="line">Where does the correction factor come from?</p></div></div><div class="dialogue"><p class="character">CARTER</p><p class="paren">(passing)</p><div class="lines"><p class="line">The table.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Whose table?</p></div></div><div class="dialogue"><p class="character">CARTER</p><div class="lines"><p class="line">Meridian's.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">Can I see it?</p></div></div><div class="dialogue"><p class="character">CARTER</p><div class="lines"><p class="line">No.</p></div></div></div><div class="screenplay"><p class="action beat">Carter keeps walking. Then she stops.</p></div><div class="screenplay"><div class="dialogue"><p class="character">CARTER</p><div class="lines"><p class="line">And don't put that in the write-up.</p><p class="line">They mark against the table.</p></div></div><div class="dialogue"><p class="character">MAYA</p><div class="lines"><p class="line">So if the table's wrong—</p></div></div><div class="dialogue"><p class="character">CARTER</p><div class="lines"><p class="line">Then you're wrong.</p></div></div></div><div class="screenplay"><p class="action">She moves to the next bench. Maya writes down the number from the table.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="artifact email"><table class="email-head"><tbody><tr><td>From:</td><td><span>Office of Student Life <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>Wednesday, 6:02 AM</td></tr><tr><td>Subject:</td><td><strong><span>You have been placed on the docket</span></strong></td></tr></tbody></table><div class="email-body"><p><span>Congratulations on beginning your first year.</span></p><p><span>Unattached students in good standing are placed on the docket in their first term. Your assignment is Thursday. Attendance is recorded.</span></p><p><span>Please arrive presentable.</span></p><p><span>Docket attendance is not a component of review. Docket attendance is recorded.</span></p></div></div><div class="screenplay"><p class="action">He reads it twice. There is one button. He presses it.</p></div><div class="screenplay"><p class="slug">INT. BECKWITH HALL — THURSDAY</p></div><div class="screenplay"><p class="action">PROFESSOR SAMUEL BENNETT does not hand anything out. Crew cut, black hair going grey at one temple only, a moustache that has been trimmed this morning. Thin, pale, and pressed at every fold.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">There's a reading list on the door and you'll read</p><p class="line">all of it, which is less impressive than it</p><p class="line">sounds, because it isn't long.</p></div></div><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">What's the class actually for?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">There used to be several hundred languages.</p><p class="line">We have records of about eighty of them and</p><p class="line">speakers of none, so what you have is this</p><p class="line">one, and everything is written in it, and most</p><p class="line">people get through their lives on about six</p><p class="line">hundred words of it.</p><p class="line"></p><p class="line">This class is the rest of the words.</p></div></div><div class="dialogue"><p class="character">ELI BROOKS</p><div class="lines"><p class="line">Can we read things that aren't on the list?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Everything written before the founding was</p><p class="line">checked. A great deal of it was wrong, and some of</p><p class="line">it was worse than wrong, and somebody had to sit</p><p class="line">down and sort it, and it took them eleven years.</p><p class="line">What came through is the list.</p></div></div><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">Who checked it?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">The people who had read all of it. That is what</p><p class="line">the word means. You cannot sort a thing you have</p><p class="line">not read, and a man who has read the whole of it</p><p class="line">and can give you the fundamentals from memory is</p><p class="line">the man you send it to. Who else would you send it</p><p class="line">to?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Since then there are the supplements. Those are</p><p class="line">open by subject, with the approvals that go with</p><p class="line">the subject. A student of biology has the biology</p><p class="line">in front of her the day she needs it, and quite</p><p class="line">right too. If you want something outside your</p><p class="line">subject, you may ask me or any approved</p><p class="line">instructor, who writes to the office, and the</p><p class="line">office writes to whoever holds it. It comes back</p><p class="line">inside a fortnight and nobody is unkind about it.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">What if you just want to read it?</p></div></div></div><div class="screenplay"><p class="action beat">Bennett takes his glasses off and looks at them.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">There's one every year. Take a seat, you haven't</p><p class="line">done anything wrong.</p></div></div></div><div class="screenplay"><p class="action beat">Eli had not stood up.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">You'd like to see a thing first and judge it</p><p class="line">afterwards. That's — well. That's a thing people</p><p class="line">say.</p></div></div></div><div class="screenplay"><p class="action beat">He breathes on a lens.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Consider what it is you're proposing, though.</p><p class="line">You're proposing that a thought of yours, which</p><p class="line">you have had since about Tuesday, has not already</p><p class="line">been had by somebody who read the whole of it and</p><p class="line">held it up against the rest, and found that it</p><p class="line">stood, or found that it didn't. Do you think</p><p class="line">that's likely?</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">It might not have been.</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">It might not. That does happen, and when it</p><p class="line">happens it is written up and sent in and read, and</p><p class="line">if it stands it goes on the list with the name on</p><p class="line">it. You don't send it in yourself, of course. I</p><p class="line">send it in, if I think it will stand. That is what</p><p class="line">an approved instructor is for.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">Have you sent anything in?</p></div></div></div><div class="screenplay"><p class="action beat">Bennett stops polishing.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Two, in nineteen years. One of them stands.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">What was it?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">A note on a translation. It's four lines, and it's</p><p class="line">on the list, and it will be on the list after I'm</p><p class="line">not.</p></div></div></div><div class="screenplay"><p class="action beat">He would be embarrassed to be told that this is the proudest thing he has ever said out loud.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">The Readers can't take it from everyone, you</p><p class="line">understand. Imagine trying to think every thought</p><p class="line">there is, only to arrive at the one you needed.</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Can you name the twenty governances on the vetting</p><p class="line">of matter held in supplement?</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">No.</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Nor could I at your age, and I would not have</p><p class="line">known which of them I was breaking. It's no mark</p><p class="line">against a soul to have had a thought. But a</p><p class="line">thought you haven't held up against the word of</p><p class="line">the people who checked it has nothing underneath</p><p class="line">it, and a thing with nothing underneath it wants</p><p class="line">trimming, and it's kinder to trim it early.</p></div></div></div><div class="screenplay"><p class="action">He puts the glasses back on.</p></div><div class="screenplay"><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">I meant stories.</p></div></div></div><div class="screenplay"><p class="action beat">The room is quiet in a way it has not been quiet yet.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">People still write them. There's a shelf for it.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">How big a shelf?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">It's a small shelf and it isn't on the list.</p><p class="line">I've read some of it, which I mention because</p><p class="line">you'll be told that I haven't.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody had been going to tell Eli anything of the kind.</p></div><div class="screenplay"><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">Was it good?</p></div></div></div><div class="screenplay"><p class="action beat">Bennett considers the question for longer than the class expects.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">It was written by people who had time. That's not</p><p class="line">a criticism, it's a fact about the conditions.</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">People write a great deal. Reports. Records.</p><p class="line">Transcripts. A man sees a thing happen and writes</p><p class="line">it down properly and that document is still true</p><p class="line">in a hundred years. It's honourable work, it's</p><p class="line">well paid, and I'd be pleased if any of you did</p><p class="line">it.</p></div></div><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">Like on the call floor show?</p></div></div></div><div class="screenplay"><p class="action beat">Bennett brightens for the first time all hour.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">The quarterly did a very good piece on that</p><p class="line">programme. Six pages. Read that.</p></div></div></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="artifact handout"><p class="handout-title">THE FLOOR · fourteenth season · episode three · transcript (approved)</p><p><span>Recorded on Array Nine. Desks in tens, tens of tens, further than the lighting goes. A management pole at every hundredth desk. Submanagers walk the aisles on a four-minute cycle. The misters run at the quarter hour.</span></p><p><span>PAULA is at desk 4,412. PAULA's feed line is dressed in the same blue as her chair.</span></p><p><span>DERMOT. Paula. Your aftercall is four seconds over.</span></p><p><span>PAULA. I know, Dermot.</span></p><p><span>DERMOT. Four seconds is ninety by Friday. Ninety is a name on a list, and I don't write the list, I only read it out.</span></p><p><span>(LAUGHTER)</span></p><p><span>PAULA. I'm bringing it down.</span></p><p><span>DERMOT. You've said that.</span></p><p><span>PAULA. I want to bring it down. I want the array to make the number.</span></p><p><span>DERMOT. (warmly) There it is. That's the mission talking.</span></p><p><span>(LAUGHTER)</span></p><p><span>DERMOT. Do you have your handbook?</span></p><p><span>PAULA. I have the handbook.</span></p><p><span>DERMOT. Are you familiar with the script?</span></p><p><span>PAULA. I'm familiar with the script.</span></p><p><span>DERMOT. Because it's all in the script, Paula. The script is four seconds shorter than what you're saying.</span></p><p><span>(LAUGHTER)</span></p><p><span>DERMOT. Would you like me to read it with you?</span></p><p><span>PAULA. ...Yes.</span></p><p><span>(LAUGHTER · SUSTAINED)</span></p><p><span>(APPLAUSE)</span></p><p><span>The production notes with pride that PAULA has not left the array in fourteen seasons.</span></p><p><span>— the quarterly, six pages, closing line: the warmth of the programme is in its accuracy.</span></p></div><div class="screenplay"><p class="action">Everyone in the room has seen it. Everyone in the room has an aunt or an uncle on an array somewhere, and when one of them comes up in conversation the number of years at the desk comes up in the same sentence, the way a rank would.</p></div><div class="screenplay"><p class="slug">INT. BECKWITH HALL — CONTINUOUS</p></div><div class="screenplay"><p class="action">Bennett goes to the door and looks at the list himself, the way you check a number you already know. He straightens it on its pin, which does not need straightening.</p></div><div class="screenplay"><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">It's four longer than it was last year.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">Four what?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Four titles. Six went to the office in November,</p><p class="line">four came back in June, and three of the four are</p><p class="line">by the same woman, in two disciplines.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">What about the two that didn't come back?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">They didn't stand.</p></div></div><div class="dialogue"><p class="character">ELI</p><div class="lines"><p class="line">That's it?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">That's the whole of it. People think the list</p><p class="line">doesn't move. The list moved four this year and it</p><p class="line">moved six the year before, and I've read every one</p><p class="line">of them, and I'd like you to read the four.</p></div></div></div><div class="screenplay"><p class="action">He comes back to the front of the room. The hour has eleven minutes left in it and he uses four of them to read out the first paragraph of the first item on the list, twice, in two different ways, and the second way is better and everybody hears that it is better and nobody can say why.</p></div><div class="screenplay"><div class="dialogue"><p class="character">A STUDENT</p><div class="lines"><p class="line">Is the list on the review?</p></div></div><div class="dialogue"><p class="character">BENNETT</p><div class="lines"><p class="line">Everything's on the review.</p></div></div></div><div class="screenplay"><p class="action">The room writes that down. Bennett watches them write down the only thing he has said all hour that was not worth writing down.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. THE COMMONS — THURSDAY, 7:00 P.M.</p></div><div class="screenplay"><p class="action">Hudreds gather in the cafeteria, known on campus as "The Commons". They never close and are never empty.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> sits at the end of a long table with three people he does not know and who are not eating together either.</p><p class="action">CHAD goes past with a tray on his shoulder and lifts a hand at him.</p></div><div class="screenplay"><div class="dialogue"><p class="character">CHAD</p><div class="lines"><p class="line">Thinking part!</p></div></div></div><div class="screenplay"><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> lifts a hand back. Chad has already gone by. He is going to a table of eleven.</p><p class="action">Two tables down, ELI is telling a story standing up.</p><p class="action">Maya is not here. Researchers eat at the benches. Nobody has told <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> this and he has not asked anybody.</p><p class="action">The three people at the end of the table are arguing about the line.</p></div><div class="screenplay"><div class="dialogue"><p class="character">A BOY</p><div class="lines"><p class="line">It's sixty.</p></div></div><div class="dialogue"><p class="character">A SECOND BOY</p><div class="lines"><p class="line">Sixty on the review. Labs are separate.</p></div></div><div class="dialogue"><p class="character">A GIRL</p><p class="paren">(It's sixty on everything and attendance is)</p><div class="lines"><p class="line">weighted.</p></div></div><div class="dialogue"><p class="character">A BOY</p><div class="lines"><p class="line">Weighted how?</p></div></div></div><div class="screenplay"><p class="action beat">None of them know. They keep going for eleven minutes.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> eats and listens and does not join in.</p><p class="action">At ten past seven he is finished and there is nothing else to do, so he goes back to his room.</p></div><div class="screenplay"><p class="transition">Cut to:</p></div><div class="screenplay"><p class="slug">INT. ASHCROFT HOUSE — ROOM 214 — FRIDAY</p></div><div class="screenplay"><p class="action">Two beds. One made. One stripped to the frame.</p><p class="action">There was a bag on it on Monday, and a coat on the hook. <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> has not met the person they belong to.</p></div><div class="screenplay"><div class="dialogue"><p class="character"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span></p><p class="paren">(into the corridor)</p><div class="lines"><p class="line">Has anyone seen Grigg?</p></div></div><div class="dialogue"><p class="character">A VOICE (O.S.)</p><div class="lines"><p class="line">Who?</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">Two fourteen.</p></div></div><div class="dialogue"><p class="character">A VOICE (O.S.)</p><div class="lines"><p class="line">Oh. He got reviewed.</p></div></div><div class="dialogue"><p class="character">A SECOND VOICE (O.S.)</p><div class="lines"><p class="line">He was carrying two.</p></div></div><div class="dialogue"><p class="character">A VOICE (O.S.)</p><div class="lines"><p class="line">You can only carry one.</p></div></div></div><div class="screenplay"><p class="action beat">Nobody comes out into the corridor.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> opens housing. Two fourteen is a single. In August it was a double and the name on it was GRIGG, N.</p><p class="action">He moves the tower to the other desk, because it is bigger.</p><p class="action">The desk rocks.</p><p class="action"><span class="pc-name" data-case="pc" data-pc="-first">Owen</span> tips it up. Under the short leg there is a wad of paper, folded into eight and gone furry along the folds.</p><p class="action beat">It is a page out of a book.</p><p class="action">One side is printed. Somebody has written on it in pencil, and the pencil is fainter than the printing.</p><p class="action">He puts the desk down on his shoe and reads it with the corner still in his hand.</p></div><div class="screenplay"><p class="transition">Blackout.</p></div><div class="artifact handout"><p class="handout-title">218</p><p><span>A set is a bag with no repeats in it. Asking a set whether it has a thing is one question, however big the bag is.</span></p><p><span>(def letters (set "banana"))</span></p><p><span>(contains? letters \a) ;; true</span></p><p><span>(contains? letters \z) ;; false</span></p><p><span>A string can be walked through one character at a time, and each character can be swapped for something else.</span></p><p><span>(map (fn [c] (if (contains? letters c) c \-)) "cabana")</span></p><p><span>Counting is not a special operation. Counting is asking how long the list of things you kept is.</span></p><p><span>EXERCISE 6.4 · The word game. Show the letters that have been guessed and hide the ones that have not. Stop after the sixth wrong guess. The player may not guess the same letter twice and be charged twice for it.</span></p><p><span>— in pencil, in the margin, in handwriting that slopes: rest of it on 219. dont lose this one</span></p><p><span>— on the reverse, a paper pocket with a card in it, and the card is stamped eleven times, and the last stamp is a date. Above the dates: 005.13 · OAKES</span></p></div><div class="screenplay"><p class="action">There is no page 219. <span class="pc-name" data-case="pc" data-pc="-first">Owen</span> looks for it for a while, in the drawer, behind the drawer, and under the other desk, which does not rock.</p><p class="action">The page gives him three things and he works out the fourth on Saturday and gets it wrong. On Sunday he gets it wrong in a different way. On Sunday night it runs.</p></div><div class="scene-code"><pre><code>(notices/render (notices/play "REYES" "AEIORSY"))
(notices/render (notices/play "WHITLOCK" "ABCDEFG"))</code></pre></div><div class="screenplay"><p class="action">It is not a good version. It counts a letter twice if you give it the same letter twice, which is the thing she said was marked, and he does not find that until Tuesday.</p><p class="action">He keeps the page in the book he does not have, which is to say folded into eight, in his bag, where it stays for the rest of the year.</p></div><div class="live-cell"><script type="application/javascript">if (typeof window.__fedorasSplitForms === 'undefined') {
window.__fedorasSplitForms = function(s){
var out=[];var depth=0;var start=-1;var inStr=false;var inComment=false;
for(var i=0;i<s.length;i++){
var c=s[i];
if(inComment){if(c==='\n')inComment=false;continue;}
if(inStr){
if(c==='\\'){i++;continue;}
if(c==='"'){inStr=false;}
continue;
}
if(c===';'){inComment=true;continue;}
if(c==='"'){inStr=true;continue;}
if(c==='('||c==='['||c==='{'){
if(depth===0)start=i;depth++;continue;
}
if(c===')'||c===']'||c==='}'){
depth--;
if(depth===0&&start>=0){out.push(s.substring(start,i+1));start=-1;}
continue;
}
}
return out;
};
}</script><div class="live-label">NOTICES.CLJ · his version, before Tuesday</div><textarea class="live-src" id="live-6d0d190ec1b8422791a8d33db756843f-src" rows="20" spellcheck="false">(def secret "BENNETT")
(def review ["REVIEW SCHEDULED" "FILE OPENED" "FIRST NOTICE"
"SECOND NOTICE" "FINAL NOTICE" "REPOSSESSED"])
(defn play [guesses]
(let [g (set guesses)
shown (apply str (interpose " " (map #(if (g %) % "-")
secret)))
wrong (remove (set secret) g)]
[shown
(apply str "wrong: " (interpose " " wrong))
(cond
(>= (count wrong) 6) "REPOSSESSED"
(every? g (set secret)) "PASSED"
(zero? (count wrong)) "in good standing"
:else (nth review (dec (count wrong))))]))
;; Observe your results, and mark them in your observation logs.
(play "ZZQ")
(play "NBT")</textarea><div class="live-bar"><button class="live-run" onclick="(function(){var s=document.getElementById('live-6d0d190ec1b8422791a8d33db756843f-src').value;var o=document.getElementById('live-6d0d190ec1b8422791a8d33db756843f-out');if(typeof scittle==='undefined'){o.textContent='(ClojureScript runtime unavailable.)';return;}try{ var forms=window.__fedorasSplitForms(s); var lines=[]; forms.forEach(function(f){ var t=f.replace(/^\s+/,''); var isDef=/^\(def[n]?\s/.test(t); try{ var v=scittle.core.eval_string('(pr-str '+f+')'); if(!isDef){ lines.push((v===null||v===undefined)?'nil':String(v)); } }catch(e){lines.push('ERR: '+String(e));} }); o.textContent=lines.join('\n');}catch(e){o.textContent=String(e);}})()">Run</button></div><pre class="live-out" id="live-6d0d190ec1b8422791a8d33db756843f-out"></pre></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">11:48 PM</span></p><div class="discord-text"><p><span>Attendance Thursday was four of five.</span></p><p><span>I will not name the fifth.</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">11:49 PM</span></p><div class="discord-text"><p><span>it was me. i had docket</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:49 PM</span></p><div class="discord-text"><p><span>I said I would not name you.</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Derek</span></span><span class="discord-at">11:52 PM</span></p><div class="discord-text"><p><span>the docket is a Roman institution actually</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Simon</span></span><span class="discord-at">11:58 PM</span></p><div class="discord-text"><p><span>did anyone in your halls get reviewed</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:04 AM</span></p><div class="discord-text"><p><span>ok</span></p></div></div><div class="discord-msg"><p class="discord-meta"><span class="discord-who"><span>Victor</span></span><span class="discord-role">DM</span><span class="discord-at">12:31 AM</span></p><div class="discord-text"><p><span>Wednesday is DnD. You are the ranger. I have assigned you the ranger.</span></p></div></div></div></div></div>
```