Guide · 02 of 15
Exercises
An exercise is the checkable part of a slide — the thing the learner answers. Every exercise shares four optional lines: ask "..." (the prompt), hint "..." (repeatable — a hint ladder), ! "..." (the explanation shown after checking), and skill: "..." (the skill it tests).
expect#
A correctness check for the author, not the learner. Goes inside a numeric exercise and states, as a formula, why the answer is what it is. The compiler works it out and refuses to build if it disagrees with answer:. It's double-entry bookkeeping for math: writing answer: 0.8 next to expect: 4/sqrt(4^2+9) means a slipped decimal becomes a compile error instead of a wrong answer a student has to argue with. Nothing about it reaches the browser. A chemistry answer can be checked the same way: unpaired("Fe3+") is the number of unpaired electrons of a species and moment("Fe3+") the magnetic moment \sqrt{n(n+2)} as a number (5.916 for Fe3+, so give the answer a tolerance to match how it is rounded). They take a quoted species and work out when the lesson compiles, so they are refused in a label, a goal or any expression that moves with the scene. Only numeric takes it, since it needs one unambiguous value to compare against.
expect: <expression>
| prop | type | description |
|---|---|---|
| expect* | expr | closed-form value the answer must match, within tolerance |
Example
reading#
expect: for a circuit question, on a quiz or numeric: a formula over the circuit's readings (I_R1, V_R1, P_R1, Req) and params. A quiz option can carry a value:. dsl verify solves the circuit at its starting state and blocks the lesson (V_CIRCUIT_VALUE) if the correct option's value is over 0.5% off, missing, or a wrong option matches too; a numeric needs an answer within tolerance of it. Needs a scene circuit. To ask about the circuit after a change, write reading: V_V1 at { k: true }: dsl verify then solves it with those params set (a number for a param, true or false for a bool, an option in quotes for a choice). The student still plays the live scene; at only changes what the key is checked against.
quiz { reading: <expr> * "2 A" { value: 2 } - "3 A" { value: 3 } }
Example
reading at#
Checks the answer against the circuit with some params overridden, so one live lab can carry a question about what happens after the switch closes or the rheostat moves. An unknown param, or a value of the wrong type, is a compile error.
numeric { reading: <expr> at { <param>: <value>, ... } answer: ... }
Example
worked ladder#
The help behind a question's I'm stuck button. A worked { } written inside an exercise of any kind is that question's ladder, not a block on the slide, so none of it shows until the student asks, and then one step at a time. Steps read like any worked block (why:, detail { }, \new{...}, closing > lines). Step 1 says what the question asks and names the idea, the middle steps do the working, and the last step lands on the answer. Three to six steps, no title, and fixed text, so no ${...}, and a numeric with vary cannot have one. dsl verify warns when the last step never reaches the answer and when an earlier step already gives it away. It earns its keep in a bank, which has no detours.
numeric { ask "..." answer: ... worked { step "..." step "..." step "..." } }
Example
exercise showme#
A goal's Show me, on an exercise of any kind. Once the last hint is out, a Show me button animates the slide's scene to these values (same rules as a goal's showme), so it needs a hint and a scene. It moves the picture, it never answers or costs credit.
quiz { ... hint "..." showme: { k: v } [dur: <ms>] }
Example
quiz#
Multiple choice. Mark the correct option with * and wrong options with -. Each option may carry a { why: "..." } giving per-option feedback shown when the learner picks it.
quiz { ask "Question?" - "wrong option" { [why: "..."] } * "correct option" [hint "..."] [! "explanation"] }
Example
numeric#
A free-entry numeric answer. The learner types a number and it counts as correct if it lands within tolerance of any listed answer. Answers fold from expressions, so 64/3 or sqrt(2) are fine. tolerance defaults to a tiny epsilon (so type an exact expected value, or widen it for estimates). Write it as a percentage, tolerance: 2%, to accept anything within that share of the answer, which is the form that fits a very large or very small answer or one that changes size under vary. The student may type scientific notation as 2.5e20, 2.5×10^20, 2.5x10^20 or 2.5*10^20, and an answer that size is shown back the same way. unit is shown as a suffix in the input. A wrong line names a specific mistake: its "why" shows when a student types that value, and -> "detour-id" sends them to a scaffold made for that misconception (see onwrong). wrong expr "6 + h" catches an answer with a letter still in it, typed in any equal form (h+6, (12+2h)/2). A letter answer is always wrong; one that matches no wrong expr gets a note that it still has a variable.
vary gives fresh numbers on every attempt. Declare vary a in range(2, 10) (or a list of values), write ${a} in the question, and write answer:, expect: and wrong as expressions in a. The compiler works every combination out and refuses to build if expect: disagrees with the answer, or a wrong value is right, for any of them. Each time a student meets the question the server hands out a new signed seed, and grading uses that seed, so it stays on the server.
numeric { ask "..." answer: <number> [answer: <another accepted value>] [wrong <value> | expr "..." ["why"] [-> "detour-id" [retry]]] [vary <name> in range(a, b) | [v1, v2, ...]] [tolerance: <n> | <n>%] [unit: "..."] }
| prop | type | description |
|---|---|---|
| answer | number | an accepted value (repeatable) |
| tolerance | number | percent | how far off is still correct (default ~0), or a share of the answer |
| unit | string | label shown next to the input |
| wrong | <value> | expr "<typed form>" ["why"] [-> "slide-id" [retry]] | a known wrong answer, its feedback, and its own detour (repeatable) |
| vary | <name> in range(a, b) | [values] | a number that changes on every attempt, usable as ${name} in the text (repeatable) |
Example
build#
Tap tokens from the bank into slots to assemble an answer. List multiple answer: lines to accept equivalent orderings (e.g. commutative forms). Add reusable when a token can be placed more than once. Bank tokens and slots render as rich text, so "$f(a)$" is a perfectly good token.
Without template, the slots are a bare row and slots defaults to the length of the first answer. With template, the slots sit inline in the text you write, which turns the same exercise into an expression builder or a fill-in-the-blank sentence. Mark each blank with ___ (three or more underscores). Underscores inside a $...$ span are left alone, so $x_1$ is safe — which also means a blank cannot go inside a math group like x^{...}; make the whole group a bank token instead. When a template is present it decides the slot count, so leave slots off.
miss "tok" "why" gives a decoy its own feedback: after a wrong check, the why of every decoy the student placed is shown, so each tempting wrong token names its own mistake.
build { ask "..." bank: ["tok", "tok", ...] answer: ["tok", ...] [answer: ["...alt ordering..."]] [template: "text with ___ blanks"] [slots: <n>] [reusable] [miss "tok" "why"] }
| prop | type | description |
|---|---|---|
| bank | string[] | the tokens the learner can place |
| answer | string[] | an accepted sequence (repeatable) |
| template | string | text the slots sit inside; each ___ outside math is a slot |
| slots | number | number of slots (defaults to first answer length; unused with a template) |
| reusable | flag | allow a token to be used more than once |
| miss | "tok" "why" | feedback shown when this decoy was placed (repeatable) |
Example
hotspot#
Tap the right spot directly on the slide's scene, rather than picking from a list or typing a value. target is a rect or circle region in scene (data) coordinates — a tap inside it is correct. target part <id> takes the region from an object on the scene instead: the box around the rect objects named <id> (a rect, or a ptable cell, row, column or block such as t_Fe or t_series1), so the question needs no coordinates. The tapped point is marked on the scene; miss is shown instead of the usual explanation when the tap lands outside the target.
hotspot { ask "..." target rect (x, y) { w: <n>, h: <n> } [target circle (x, y) { r: <n> }] [target part <object id>] [miss "..."] }
| prop | type | description |
|---|---|---|
| target* | rect | circle | part | the hit region, in scene coordinates |
| miss | string | feedback shown when the tap misses |
Example
sketch#
Draw directly on the slide's scene — a freehand curve, a handful of tapped points, or a dragged straight line. curve and points check against near landmark points (the drawn shape must pass within tol of each); curve is checked as a connected stroke, points as independent taps. Give a curve a follows: function and the whole drawing is graded against it over over: (or the span of the near points), which is what you want whenever the shape matters and not just a few landmarks. Add overlay and, once checked, the real curve draws in over the student's stroke with the gap between them shaded, red where they were off by more than tol, so they see exactly where the prediction went wrong. line checks the drawn segment's slope against slope (within slopeTol) and that it passes within tol of through. Pairs naturally with a reveal { ... } block for predict-then-reveal.
sketch curve { ask "..." [follows: <expr in x>] [over: [start, end]] [overlay] [near (x, y) ...] [tol: <n>] } sketch points { ask "..." near (x, y) [near (x, y) ...] [tol: <n>] } sketch line { ask "..." through (x, y) slope: <n> [tol: <n>] [slopeTol: <n>] }
| prop | type | description |
|---|---|---|
| near | (number, number) | a landmark point (repeatable) |
| follows | expression in x | the function the drawn curve has to match (curve mode) |
| over | [number, number] | the x range follows: is checked on (default: the near points) |
| overlay | flag | after checking, draw the follows: curve over the sketch and shade the gap |
| through | (number, number) | point the line must pass near (line mode) |
| slope* | number | expected slope (line mode) |
| tol | number | position tolerance, in scene units |
| slopeTol | number | slope tolerance (line mode, default 0.5) |
Example
moves#
Pick the next move. The student sees an expression and chooses what to do to it; the right choice turns it into the next line, a wrong one shows its why and they choose again. It reads like a worked solution the student drives, which is where procedural fluency gets built. Every move needs one * right choice and at least one real wrong one, shuffled on screen. The exercise counts as correct only when every move was right first time.
moves { ask "..." from "starting expression" move "what it becomes" { * "the right next move" - "a tempting wrong move" { why: "..." } } [move ...] }
| prop | type | description |
|---|---|---|
| from* | string | the starting expression |
| move* | "result" { * "right" - "wrong" } | one step: what the expression becomes, and the choices (repeatable) |
Example
Pick the next move
essay#
An open question the engine cannot check: explain why, derive, state the law. The student answers on their own, taps to see the model answer written in the > lines, then grades themselves got it, part of it or missed it. That grade is the check: there is no Check button, only a full "got it" counts as right, and "part of it" brings the question back sooner in review without counting as a lapse. The model answer is rich text like slide prose, so $$...$$, - lists and role tints all work in it. Write it the way a marker wants it: the key point first, short enough to compare against at a glance. Reach for it in a bank for the book's written questions; in a lesson, a quiz or numeric that can actually be checked is nearly always the better step.
essay { ask "..." > the model answer [> more lines of it ...] [hint "..."] [! "a note shown after the grade"] }
| prop | type | description |
|---|---|---|
| >* | prose lines | the model answer the student compares against (at least one line) |
Example
Answer it yourself first, out loud or on paper. Then compare.
match#
Tap-left-then-tap-right pairing. Tap a left item, then tap the right item it belongs with — at least two pair lines are required. decoy adds extra right-side entries that never pair with anything, making the match harder to guess. The right column is shuffled (seeded off the left texts, so it stays put across re-renders of the same exercise). miss "left" -> "right" "why" names one tempting wrong pairing and explains the mistake; it shows after a check that made that pairing. A trailing -> "detour-id" sends that pairing to its own detour (see onwrong), the first listed if a check makes several.
match { ask "..." pair "left" -> "right" [pair "left2" -> "right2" ...] [decoy "extra wrong right"] [miss "left" -> "wrong right" "why" [-> "detour-id" [retry]]] [hint "..."] [! "explanation"] }
| prop | type | description |
|---|---|---|
| pair | "left" -> "right" | a correct pairing (repeatable, min 2) |
| decoy | string | an extra unpaired right-side option (repeatable) |
| miss | "left" -> "right" "why" [-> "detour-id"] | feedback for one wrong pairing (repeatable) |
Example
Tap one on the left, then its partner on the right. They turn the same color. Tap either one again to undo.
Match with
order#
Put a shuffled list of items back into the right order. The item lines are declared in their correct sequence — that's both the answer and the source of the tokens shown (shuffled) to the learner. decoy adds extra tokens that don't belong in the sequence at all. Like build, but without the operator/operand token styling and without separate bank:/answer: arrays — there is exactly one correct order.
order { ask "..." item "..." [item "..." ...] [decoy "..."] [hint "..."] [! "explanation"] }
| prop | type | description |
|---|---|---|
| item | string | a step in the correct sequence (repeatable, min 2) |
| decoy | string | an extra token that isn't part of the sequence (repeatable) |
Example
sort#
Drop each item into the bin it belongs to. Declare at least two bin groups; every item listed under a bin is shown in a shuffled tray and has to be put back. Reach for this when the lesson is about telling cases apart (which rule opens a derivative, which kind of discontinuity a function has), where a multiple choice only ever probes one case at a time. An item may appear in exactly one bin, or the compiler refuses to build, since there would be no single right answer. Items and bin labels render as rich text, so "$x^2\sin x$" is a fine item.
sort { ask "..." bin "Label": ["item", "item", ...] bin "Other label": ["item", ...] [hint "..."] [! "explanation"] }
| prop | type | description |
|---|---|---|
| bin | string: string[] | a labelled bin and the items belonging in it (repeatable, min 2) |
Example
table#
Fill in the blanks of a function table. Each row: line lists one value per column — plain numbers are given, blank(<answer>) marks a cell the learner fills in (the argument is the expected value, and folds from an expression like any other answer). Every row needs the same number of cells; at least one cell across the whole table must be a blank(...). tolerance applies to every blank. Correctness is shown per-blank plus an overall "X / N correct" count — partial credit is surfaced but never blocks Continue, same as every other exercise. One vector: line in place of the rows asks for a vector, shown as (□, □, □), or as □ î + □ ĵ + □ k̂ with vector ijk:, each component typed and graded like a blank. A blank takes wrong lines in braces after it, like a numeric's: the why shows when that blank holds that value, and the detour follows onwrong, the first blank in reading order winning.
table { ask "..." [header: ["col", "col", ...]] [rows: ["label", "label", ...]] row: <value|blank(answer)>, <value|blank(answer)>, ... [row: ...] [tolerance: <n>] } vector [ijk]: <value|blank(answer)>, ... (in place of the rows) blank(answer) { wrong <value> ["why"] [-> "detour-id" [retry]] }
| prop | type | description |
|---|---|---|
| header | string[] | column headings (optional) |
| rows | string[] | one label per row, shown at its start, so the ask need not list the order |
| row | value, value, ... | one row — mix plain numbers and blank(answer) cells (repeatable) |
| vector | [ijk] value, value, ... | one vector answer in place of the rows, two or three components with ijk |
| tolerance | number | how far off a blank can be (default ~0) |
Example