Lessons for CheatSpecter's Learn section
This folder is what the Learn page in CheatSpecter shows. The app does not ship with
lessons: when someone opens Learn and presses "Load lessons", their browser downloads
index.json and then each lesson from this folder on GitHub. Change a file here and everyone
gets the new version the next time they open the page.
Lessons are data, never code. A lesson cannot contain HTML, scripts, links or images. The app checks every file it downloads against the format below and refuses the whole file if anything is off, so a typo here means a lesson that will not open. Run the checker before you commit:
node scripts/check-lessons.mjs
It prints what is wrong and where (lessons/find-a-number.json: lesson.steps[2].goal.field: …),
or 12 lessons, 76 steps: all fine. The test suite (test/learn.test.mjs) runs the same check,
and then has a scripted learner complete every practice step, so a goal that cannot be reached
fails the tests.
To try your lessons in the app before they are on GitHub, run npm run demo (or npm run dev):
with the demo console running, the app loads this folder from your computer instead.
Files
learn/index.json the courses, and which lessons are in them, in order
learn/lessons/<id>.json one lesson; <id> is the lesson's id
index.json
{
"format": "cheatspecter-lessons",
"version": 1,
"edition": "2026-10",
"courses": [
{
"id": "values",
"title": "Finding and changing values",
"summary": "One or two sentences shown under the course title.",
"lessons": [
{ "id": "find-a-number", "title": "Find a number you can see", "summary": "…", "minutes": 8, "practice": true }
]
}
]
}
title, summary and minutes must be the same as in the lesson's own file (the list of
lessons is drawn from the index alone). practice is true when the lesson has at least one
practice step. edition is shown at the bottom of the list; change it when you publish. Ids are
lower-case letters, digits and dashes.
A lesson
{
"format": "cheatspecter-lesson",
"version": 1,
"id": "find-a-number",
"title": "Find a number you can see",
"summary": "Search for your ammo, change it in the game, search again, and edit what is left.",
"minutes": 8,
"games": { "range": { … }, "shop": { … } },
"steps": [ … ]
}
games holds the practice games the lesson's steps use, by a name of your choosing. steps
are shown one at a time, in order.
Text
Wherever a step has text (and in done and explain), this small markup works. Anything
else is shown as the characters you typed.
| You write | You get |
|---|---|
| a blank line | a new paragraph |
# Heading on its own |
a heading |
lines starting with - |
a bulleted list |
lines starting with 1. , 2. |
a numbered list |
> note: …, > tip: …, > warning: … |
a callout |
**bold** |
bold, for the names of buttons and for new words |
`code` |
code, for instructions, addresses and file names |
Write for someone who has never done this. Name buttons exactly as the app shows them, and say what the learner will see after each action.
Steps
Every step has a kind and a title.
read: something to read. text.
choice: a multiple-choice question. question, options (2 to 6 strings), answer
(the position of the right one, counting from 0), explain (shown once they get it right), and
optionally text above the question.
answer: a question answered by typing. question, accept (the answers that count;
capitals, spacing and a final period are ignored), explain, optionally text.
practice: the learner does something in a practice game with the real tools.
{
"kind": "practice",
"title": "Fire, then narrow down",
"text": "What to do, step by step.",
"game": "range",
"tool": "find",
"goal": { "check": "narrowed", "label": "Narrow the list down to one match", "field": "player.ammo", "atMost": 1 },
"hints": ["A nudge.", "A stronger nudge.", "Nearly the answer."],
"done": "Shown when the goal is reached: what just happened, and why it matters."
}
game: which of the lesson's games. Steps in the same game carry on in it (the learner's search and cheats are still there), also when a reading or a question sits between them, unless a step has"fresh": true, which starts a new game with no cheats. Mark a stepfreshwhen it does not use anything from the steps before it.tool: the tab shown when the step opens:find,memory,codeorcheats.hints: one to six, revealed one at a time, weakest first."challenge": truemarks a step the learner does alone. Give the goal and no steps. Every practical lesson should end with one, in a game with different numbers from the guided part.
The learner also gets a Show me button. It reaches the goal with the same tools and writes down what it did; you do not script it.
Writing practice steps that hold up
A learner does not always arrive at a step the way you wrote it: they skip, come back the next day (the game starts over), restart the game when not asked, or type the wrong number. The app helps: when a step builds on earlier ones that were not done in the running game, it says so and offers the way back; and when a search has dropped the value a goal is about, it says that too. The text has to do its part:
- Call things what the app calls them. Bold in a practice step is for what the learner
presses or looks at, and the checker refuses a bold name that is neither in the app nor in
the game. (Examples to type, such as a cheat's name, go in
EXAMPLESin the checker.) - Do not say where on the screen something is. The practice game is at the bottom of a phone and beside the lesson in a wide window; a table has column headings on one and not on the other. Say "the practice game's panel", "the box on the row".
- Do not assume a button that only exists in one state. "Press New search" is wrong when no search is going; write "if the Find tab shows a search, press New search".
- Follow your own hints in the app, literally, in the game as it starts. A game is laid out the same way for every learner, so what you see is what they get: if the pointer search offers two paths, the hint has to say which one.
- Order a goal's parts so that only the real thing passes. "Make a cheat that survives the
update" is
off, thenupdated, thenoffagain; withupdatedfirst, a cheat made after the update would pass. - An older CheatSpecter reads these same files. Stay within the format, and keep text true for it as well: in 0.4.0 the game started over after every reading or question.
soon: text explaining practice that the app cannot offer yet.
Goals
A goal is what the app checks for. Each has a label: the sentence the learner sees with a
circle or a check mark next to it. field means a value in the game, written object.field;
action means one of the game's actions.
check |
Reached when | Also needs |
|---|---|---|
narrowed |
the learner's search results contain the value and are at most atMost long |
field, atMost |
value |
the value is atLeast, atMost or exactly a number |
field, is, value |
held |
the value is unchanged after each of times uses of an action in a row (frozen, or its code is switched off) |
field, action, times |
steady |
a value the game changes by itself has not moved for seconds |
field, seconds |
pressed |
an action has been used times times |
action, times |
restarted |
the game has been restarted | |
updated |
the game's update has been installed (the game needs "update": true) |
|
caught |
the debugger has reported the code that writes the value ("Find what writes to this") | field |
off |
the code behind the action's first effect is replaced with no-ops | action |
saved |
"Your cheats" has a cheat of type value (for a field) or patch (for an action). Add "named": true to require a name other than the automatic one, "grouped": true to require a group, "location": "pointer" to require a pointer path |
type, and field or action |
To ask for several things, use { "check": "all", "of": [ … ] } with two to five goals. They
count in order: a goal is only looked at once the ones before it are reached, and what it
counts (restarts, uses of an action) is counted from that moment. So "restart, then switch
the cheat on again" is restarted followed by off.
Practice games
A practice game is a description; the app builds a small pretend game from it, with real memory to search and real machine code behind every action.
{
"name": "Specter Range",
"objects": [
{ "id": "player", "fields": [
{ "id": "ammo", "type": "u32", "value": 30, "offset": 40 },
{ "id": "shots", "type": "u32", "value": 0, "offset": 64 }
] }
],
"actions": [
{ "id": "fire", "label": "Fire", "effects": [
{ "field": "player.ammo", "op": "sub", "by": 1, "min": 0, "refill": 30 },
{ "field": "player.shots", "op": "add", "by": 1 }
] }
],
"hud": [ { "label": "Ammo", "field": "player.ammo", "show": "number" } ],
"decoys": [ { "type": "u32", "value": 30, "count": 12 } ],
"copies": [ { "of": "player.ammo", "count": 3, "follow": "fade" } ],
"drifting": 0,
"update": false
}
objects(1 to 8) are the things the game keeps in memory. Eachfieldhas atype(u8,u16,u32,float,double, orpointer), a startingvalue, and optionally anoffsetfrom the start of the object (a multiple of the type's size; left out, fields follow each other from 16). Apointerhasto(another object's id) instead of a value. Objects land somewhere new every time the game starts. One with"static": truelives in the game's own data, at the same place every time: give it apointerto another object and there is a pointer path to find.actions(up to 8) become buttons. Eacheffectadds to or subtracts from au32,floatordoublefield. A subtraction that would go belowminsets the value torefill(or stops atmin); an addition stops atmax. An action with"every": 1000runs by itself every that many milliseconds and has no button ("hidden": truehides one without a timer). Each effect is its own few bytes of code, which "Find what writes to this" finds and no-ops switch off. Effects that name the same"site"share one piece of code, as "take damage" for the player and for an enemy would; they must do the same thing to the same field of their objects.hud(up to 6) is what the game shows: anumber(withdigitsdecimals), abar(filled atmax; the number is not shown, which is the point), or atime(seconds shown as m:ss).decoysput the same number in unrelated places, so a first search finds more than one.copiesfollow a value for a few changes and then stop, as a game's own copies of a number do;count3 means narrowing down takes about four rounds.driftingis how many values change on their own, which gives "stayed the same" a job in searches that start from "Unknown".updatetruegives the game an "Install game update" button that moves its code.
If someone overwrites a game's code with anything other than whole instructions of no-ops, the game "crashes" the next time that code runs, and "Restart game" brings it back.
Changing the format
version is 1. The app refuses files with a higher version and tells the user to update, so
new kinds of steps or goals mean a new version number and a new app release first. The types
and the checker are in src/lib/learn.ts; the practice game is in src/lib/practice/.