The .wplay format

The same page as plain Markdown, for an agent: format.md

What a wordplay is

A wordplay is a teacher's understanding, frozen into a file, that a tutor defrosts in conversation with one student at a time. The file holds three things: what the student plays with, when each part of it appears, and what the teacher prepared for the tutor. A tutor takes those three and holds the session the teacher would have held.

The file

A .wplay file is an HTML document with four attributes on it. ARIA added attributes to HTML so a screen reader could read a page; these four add what a tutor needs on top of that.

AttributeOn any element, means
data-wordplay="1"On <body>: this document is a wordplay.
data-contextThe student never sees this element. The tutor reads it, and operates it if it is a control.
data-expect="…"This element is given once a condition holds, judged by the tutor from the conversation, or said by the page's script.
data-tutorOn an <output>: the tutor writes it, by its accessible name. What it writes replaces what was there.

Everything else is HTML, written to ARIA, and is shown as HTML. A relative URL in it resolves against where the file is. What is logical, checking an answer or counting a game, is the teacher's script (Scripts, below).

Rules

One rule. An element with a condition is given once its condition holds: to the student, or, if it is marked data-context, to the tutor. Until then it is not given at all, with everything it holds. An element without a condition is given with its parent. The body is always shown.

Steps and reveals. A <section> the student sees that carries a condition is a step, and steps are the progress (below). Any other element the student sees with one is a reveal, a hint or a figure, given the same way and not counted; a hint is <aside data-expect="…">. A condition goes on what the student should see next, written as the moment that earns it.

Order. A condition is judged once its element's parent has been given. Conditions do not wait for one another otherwise, so a hint the student never needs blocks nothing after it. Two steps that must come one after the other nest, or say in their conditions what they follow.

Context. A context element holds whatever the teacher wants the tutor to read; the tutor is given its text, with inline markup kept as written, so math and code survive. Context with no condition is given as soon as its parent is; with one, once the condition holds. Context nests by nesting.

The condition. data-expect is prose, the moment as the teacher would recognize it, judged by the tutor or said by a script (document.wordplay.met).

Ids. Each step, reveal and context element is named by its own id, else numbered in document order: s1, s2, … for steps, r1, … for reveals, c1, … for context. The tutor and scripts say what holds by id.

Reading the page. The tutor is given the page as a screen reader would read it: the accessibility tree (role, name, state) of what is shown, and what its live regions have announced. Nothing about how the page is drawn reaches it. A page that works for a blind student works for the tutor.

Acting on the page. Anything a student could do with a keyboard, the tutor can do by accessible name: press a button, set a field, choose an option. An element marked data-context is removed from the student's page and their screen reader, and placed in the tutor's tree alone.

Writing on the page. An <output> marked data-tutor is the tutor's to write, by its accessible name: what it writes is the element's text, replacing what was there. Nothing else on the page takes the tutor's words, and how they are drawn is the teacher's.

Privacy. Every element marked data-context is stripped before anything reaches the student's browser. This is the one rule a host must never break. A step or reveal reaches the browser only once given, without its data-expect, which is often the answer. Where a host lets readers share (Wordplay's share, below), what a reader shares is seen by every reader of the wordplay, with their profile only where the teacher asks for names and the reader has not chosen to be anonymous.

Unknowns. A host ignores an attribute it does not know and shows the element as HTML.

What the tutor is given at any moment. The tree of what is shown, its announcements, what the page told it since its last turn, and what was worked out on a calculator. The context given so far. Each part not yet given whose parent has been, by id and condition, so it knows what to watch for. The steps the student skipped. Where a student is is never in the file; it is worked out from their conversation. With no conversation, a host shows the whole wordplay.

Scripts

The teacher's scripts run as on any page. Before the body is read, the host puts one object on the document, document.wordplay, frozen, with these members:

MemberDoesAt most
met(id)That element's condition holds: the host gives it, as if the tutor had said so. An id not waiting is ignored.
tell(text)The text reaches the tutor at its next turn. A live region is read out to the student too; this is not.200 characters
say(text, chat?)A message from the student, as if typed, in the chat named or the tutor's own: the tutor answers it now, and is told it came from the page. Returns a promise of the reply as the student reads it, once it ends; refused, it rejects with why. tell waits for the next message; say is one.2,000 characters, 20 a minute
keep(value)Keeps one JSON value, the page's own state, with the conversation; each call replaces the last. A larger value is dropped, with a warning in the console.65,536 bytes
keptThe value last kept in this conversation, or undefined; there before the first script runs.

wordplay:met is fired on a part as it is given, bubbling, its id in event.detail.id, and again for each part already given when the page is drawn, in document order, so a script knows what is on the page whether it was given now or before.

The page is the conversation's. The host keeps each control's value, typed, checked or chosen, by its id, else its name, else its accessible name, and which page was open; drawing the page again, it sets them back and fires input and change, so a script that draws from its fields draws again. What lives only in a script's variables is keep's:

// A pile drawn in <output id="pile">, taken from by <button id="take">.
const game = document.wordplay?.kept ?? { pile: 21 };
const draw = () => (document.querySelector("#pile").textContent = game.pile);
document.querySelector("#take").onclick = () => {
  game.pile -= 1;
  document.wordplay?.keep(game);
  draw();
  if (game.pile === 0) document.wordplay?.met("emptied");
};
draw();

The kept value is never the tutor's, which reads the page as shown. It is one conversation's: starting over starts from the file. It is one value, not a store: what the page needs to draw itself again.

Opened as plain HTML a page has no document.wordplay, so a script calls document.wordplay?.met("solved"). A host may add members of its own ("What Wordplay adds"), which a script tests for: document.wordplay?.share?.(…). A script runs in the student's browser, which can read it: a secret goes in data-context or behind a data-expect, never in a script.

Progress

Progress is the steps given over the steps in the file, nested ones counted flat; it says how many, never in what order, since steps do not wait for one another. Done is every step given; a wordplay with no step is never done. Skip, where a host offers it, gives one waiting step as if its condition held, and nothing else; what is inside it then waits as usual. A step with data-skip="false" has none, and the student must earn it. A step skipped counts, and the tutor is told of it. A part met gives is the browser's report, counted apart from what the tutor gave.

Example

Page 2 of The game from Marienbad:

<!doctype html>
<html lang="en">
<body data-wordplay="1" data-chat data-progress data-skip>

<div data-context>The game is Nim with one row. The computer plays perfectly.</div>

<section aria-label="One row, small steps">
  <p>There is only one row of 21 matches. On your turn you take 1, 2 or 3. The
  player who takes the last match wins. You go first. Play a few games, look
  for a pattern, and tell your tutor your idea.</p>

  <div class="matches" role="group" aria-label="21 matches. Take 1, 2 or 3.">…</div>
  <p aria-live="polite">Your move.</p>
  <button>Start again</button>
  <form aria-label="Set up a position" data-context>…</form>

  <aside id="stuck" data-expect="they have lost three games in a row">
    <p>Try a smaller game first: who wins with 4 matches on your turn?</p>
  </aside>
  <script>
    // The game calls gameOver at the end of each game.
    let lost = 0;
    function gameOver(won) {
      lost = won ? 0 : lost + 1;
      if (lost === 3) document.wordplay?.met("stuck");
      if (won) document.wordplay?.tell("They beat the computer from 21 matches.");
    }
  </script>

  <div data-context data-expect="they say the trick is to always take 1">
    It beats a careless player and loses to the computer. Ask what happens from 4.
  </div>
  <div data-context data-expect="they ask why 4 is the bad number">
    From 4, any move leaves 1, 2 or 3, and the opponent takes the rest.
    <div data-context data-expect="they ask whether it works when the last stick loses">
      Everything shifts by one: 1, 5, 9, 13, 17, 21.
    </div>
  </div>

  <section aria-label="One row solved"
    data-expect="they say, in their own words, always leave a multiple of 4">
    <p>You found the winning strategy.</p>

    <p><strong>Exercise.</strong> On <em>Fort Boyard</em>, candidates play this
    game against the Master of Time. 21 sticks, take 1, 2 or 3, the candidate
    goes first, and the player who takes the last stick <strong>loses</strong>.
    What is the strategy now? Can the candidate win at all?</p>

    <div class="matches" role="group" aria-label="21 sticks. Take 1, 2 or 3. The last stick loses.">…</div>
    <p aria-live="polite">Your move.</p>

    <section aria-label="Fort Boyard solved" data-skip="false"
      data-expect="they say the candidate cannot win from 21">
      <p>Right: 21 is one more than a multiple of 4, so the Master of Time
      always wins.</p>
    </section>
  </section>
</section>

</body>
</html>

Opened as HTML, it is the wordplay without the tutor. As a tree, conditions on the edges:

context c1  (always)
context c2  (always: the set-up form)
reveal stuck  ── expect: they have lost three games in a row
context c3  ── expect: they say the trick is to always take 1
context c4  ── expect: they ask why 4 is the bad number
└── context c5  ── expect: they ask whether it works when the last stick loses
step "One row solved"  ── expect: they say, in their own words, always leave a multiple of 4
└── step "Fort Boyard solved"  ── expect: they say the candidate cannot win from 21

Two steps: the hint counts for nothing, and the second has no Skip.

What Wordplay adds

The above is the standard. Wordplay's site adds tooling that another host may ignore, and the file loses nothing. Nothing of it is drawn unless the file asks: a wordplay with none of it is its HTML, in the site's reading column. What is drawn around the page is declared on the <body>, each a boolean attribute with no value, as <video controls> declares its player; what is drawn in the page is an element in it.

  • The <body>'s declarations: data-chat, the tutor's chat beside the page; data-progress, progress as a thin bar over the page that only shows it; data-pages, each <section> directly in the body without a condition drawn as a page, with the pages along the foot; data-skip, Skip at each waiting step, where it will appear.
  • With data-pages, a link to an id on another page turns to that page. A link to anything else opens it: another wordplay on the site in place of this one, with the way back over its title; anything off the site in a new tab. A wordplay is linked by its name, <a href="sodium-2">, which resolves beside this one.
  • Chats in the page (data-chat on a <div>), so a question is asked where it arises. A chat in the page is one conversation with the tutor's, shown in it as a thread: the tutor reads every message in order, knowing where it was said, and answers there. A chat holding an output the tutor writes is drawn by the teacher: Wordplay draws no box in it.
  • Mathematics: $…$ in text is a formula and $$…$$ one on its own line, drawn with KaTeX, except inside code, pre, script, style and fields. The tutor reads the source as written.
  • A calculator for number answers, checked by the component's script.
  • A step or reveal given carries data-met, its id, and fires a wordplay:met event as it appears and again as the page is drawn, the id in event.detail.id, for the teacher's CSS (body:has(#solved) .hint { display: none }) or scripts ("Scripts").
  • A wall every reader of a wordplay shares, written by the page's scripts ("Sharing", below).
  • Limits on size, and checks before a wordplay is published.

Its attributes:

AttributeOn any element, means
data-chatOn the <body>: the tutor's chat beside the page. On a <div>: a chat with the tutor drawn in it, after what it holds, named by its accessible name; context inside it is given with it. The tutor answers in each chat and writes where the teacher marked; what has no chat to be said in is said nowhere.
data-progressOn the <body>: progress drawn over the page.
data-pagesOn the <body>: the sections directly in it are pages, one shown at a time.
data-skipOn the <body>: Skip at each waiting step. On a step, data-skip="false": none for it, and the student must earn it.
data-calculatorOn an <input>: the box has the calculator, which takes its place, the box's value what it works out (86.4 s). Its accessible name is its label. On a chat: one under it, = adding to the message. Its keys are the data-key elements in the chat, or in the input's <label>.
data-key="…"On a <data>: a key on that calculator, never shown in the page. number (its text: <data data-key="number">24 h</data>), constant (its text the name, its value the value: <data data-key="constant" value="9.81 m/s^2">g</data>), unit, or function (a key from the calculator's library, by name).
data-person="…"Filled with the profile of the reader of an entry on the wall: mine, or an entry's place in entries, from 0 ("Sharing").
data-opening="…"On the <body>, the tutor's first line in its chat beside the page; on a chat, its first line in that chat. Shown before anything is said, Markdown and math as in any reply, and the tutor is told it. Without it, the host's own line.

The name of a step, in the chat, on the page and in the creator, is its accessible name: its aria-label, else its first heading, else its id.

A key with a condition is put on the calculator once it holds, named k1, k2, … when it has no id. It counts for nothing, and has no Skip.

The keys a calculator may hold from its library, by name:

  • Math: sqrt, cbrt, nthroot, square, cube, power, inverse, frac, abs, factorial, percent, choose, pi, e, ans
  • Trig: sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, deg
  • Logs: ln, log, exp, tenpow
  • Greek: theta, alpha, beta, gamma, delta, lambda, mu, omega, epsilon, rho, sigma, tau, phi, psi, eta, nu, Omega, Sigma, Phi
  • Calculus: integral, sum, product, derivative, partial, limit, infinity, nabla, contour, doubleintegral
  • Relations: leq, geq, neq, approx, propto, equiv, plusminus, implies, iff, to
  • Sets: elem, notelem, subset, union, intersect, emptyset, forall, exists, reals, integers
  • Vectors: vector, hat, cross, dotproduct, norm, matrix, transpose, bar
  • Chemistry: charge, subscript, yields, equilibrium, heated, concentration, aqueous, solid, liquid, gas, change

Sharing. Each reader has one entry on the wordplay's wall, which the page's script writes, and every reader reads the whole wall:

MemberDoesAt most
share(value)This reader's entry, any JSON, replacing their last; null takes it away. A value the block below refuses is not sent, with a warning in the console.4,096 bytes, 60 a minute
sharedThe wall, { mine, entries, count, tally }: this reader's entry, every reader's newest first (200 unless the block says), how many in all, and tally, for each field the block names: { [value]: n } for a choice or boolean field, { n, sum } for a number field, over every entry and not only those sent. An entry is { value, at, guest, by?, who?, facts? }. There before the first script runs.
person(who)The site's chip for an entry's who, or the anonymous one for null.

wordplay:shared fires on the document when the wall changes. An element marked data-person is filled with its entry's chip.

What the page shares is the reader's claim, since the page runs in their browser. by: "tutor" (the tutor's judgement) and facts (the server's count of the attempt) are not: rank by those where fairness matters.

One optional <script type="application/wordplay-shared"> bounds an entry, as JSON, every key optional:

KeyMeans
fieldsThe entry is an object of these fields only: { "type": "number", "min", "max" }, { "type": "choice", "of": [...] }, { "type": "boolean" } or { "type": "text", "max" } (at most 280)
namestrue: each entry carries who, its reader's name, handle and picture, null for a guest or a reader anonymous by choice
guestsfalse: a guest of a device may not share
entriesHow many are sent, at most 1,000
factsAny of "seconds" (start to done), "steps" (the tutor gave), "messages", "skips"
tutortrue: the tutor may write a reader's entry as it judges it
tallyThe names of fields to total over every entry: for a choice or boolean field, how many entries hold each value; for a number field, how many and their sum. A text field cannot be tallied

The class's guesses, shown once the student has guessed:

<body data-wordplay="1" data-chat>
  <script type="application/wordplay-shared">
    { "fields": { "guess": { "type": "number", "min": 0, "max": 100 } } }
  </script>
  <label>Your guess <input id="guess" type="number"></label>
  <button id="send">Send</button>
  <p id="guessed" data-expect="The student has guessed">The class: <output id="class"></output></p>
  <script>
    const draw = () => {
      const all = (document.wordplay?.shared?.entries ?? []).map((one) => one.value.guess);
      const out = document.querySelector("#class");
      if (out) out.textContent = all.join(", ");
    };
    document.querySelector("#send").onclick = () => {
      document.wordplay?.share?.({ guess: Number(document.querySelector("#guess").value) });
      document.wordplay?.met("guessed");
    };
    document.addEventListener("wordplay:shared", draw);
    document.addEventListener("wordplay:met", draw);
  </script>
</body>

Standing. A wordplay reads where its reader stands on the wordplays it links to, and on itself:

MemberDoes
standing(name?)For a wordplay this file links to, by the name its link uses (<a href="sodium-2">), { title, completed, minutes, mine }: its title; how many readers finished it, each once; its length in minutes as the site says it, null until measured; and mine, this reader's own, null until they start it, else { done, given, total }. With no name, this wordplay's own. A name the file does not link to answers null. There before the first script runs, and not changed while the page is open.

A link to a wordplay is marked by the host as its reader stands on it: data-started while a conversation of theirs is under way, data-done once any of theirs finished it, so a page colors its links in CSS alone. A guest of a device has mine for this device's conversations. Another host answers zero counts and null for every name.

Checks. Wordplay says what looks like a mistake: a step with no accessible name, an id used twice, a chat unnamed, named as another or for its box ("Chat about …"), a key outside a calculator or of an unknown kind, an output the tutor writes unnamed, named as another, named with a colon or inside context, data-tutor on anything but an <output>, an attribute nearly one of these or no longer read, document.tutor, the object's old name, in a script, data-opening on anything but the body or a chat or over 400 characters, a declaration on the body with a value or on anything but the body, data-skip on anything but the body or a step or with any value but "false" on a step, data-skip="false" in a wordplay that offers no Skip, an opening line on the body of a wordplay with no chat beside the page, a condition in a wordplay with no chat anywhere, and a shared block it cannot read. It refuses to publish an unknown data-wordplay version, a file over the limit, an id used twice, a shared block it cannot read, and a control the tutor cannot see: one with no accessible name, on a page where nothing is named or live.

Limits

WhatAt most
The file400,000 characters
A condition400 characters
One live announcement, or one tell200 characters
One line the tutor writes400 characters
One say2,000 characters
An opening line (data-opening)400 characters
What a page keeps (keep)65,536 bytes
One entry shared (share)4,096 bytes, 60 a minute
A shared text field280 characters
Entries sent1,000
Keys in one group of a calculator12

Versions

data-wordplay="1" is redefined by this document. The format is a prototype: a form it drops is no longer read, and published wordplays are written again. There is no custom element. The extension stays .wplay: HTML to run with a tutor; editors highlight it as HTML.

Words

One word per thing, in the file, the site, the prompts and the docs.

WordMeans
wordplayOne .wplay file, what a reader works through
studentThe person working through it
teacherWho wrote it
tutorWhat the student talks to
hostWhat runs the file: Wordplay's site, or another
stepA <section> the student sees with a condition; it counts
revealAnything else the student sees with a condition; it does not
contextAn element only the tutor reads
chatThe tutor's chat beside the page, or one in it (data-chat)
expectThe condition, the moment as the teacher wrote it
givenShown to the student, or, for context, read by the tutor
waitingNot given, its parent given: its condition is being judged
progressHow many steps are given, of the steps
doneEvery step given
skipGiving one waiting step without its condition