Typing practice app with several modes:
classic: standard text typingracing: race opponents while typing; the track follows the rendered passage and adapts when its container becomes visible or resizesmeteoriteRain: type a falling meteorite's complete word, then press Space to submit it and destroy the meteoritetowerDefense: type prompted words to defeat enemies before they reach the castleaudio: the target text is spoken aloud (Web Speech API) and hidden; the user transcribes what they hear
git submodule update --init --recursive
npm install# local development (Vite on 3000 + API server on 3001)
npm run start:dev
# production build output to dist/
npm run build
# serve dist/ on 3000
npm run start:prodnpm start maps to npm run start:prod.
Runtime behavior is controlled by client/config.json:
gameType:classic,racing,meteoriteRain,towerDefense, oraudiokeyboard: show/hide visual keyboardavailableKeys: allowed keys (empty array means all keys)allowMistakes: whentrue, wrong characters are accepted (shown as incorrect) and typing continues instead of being rejected — natural typing where accuracy and "errors left" reflect real performance; completion is reaching the end of the text. Defaultfalse(guided mode: wrong keystrokes are rejected and must be corrected to advance).showStats: show final stats dashboardrealTimeStats: enabled live metrics (speed,accuracy,time,errors,errorsLeft,chars)includeTranscript: whentrue(any mode), the savedstats.txtalso includes the expected (reference) and submitted (typed) transcriptions, so a grader can compare the actual transcription — not just the numbers. Defaultfalse.gradeMode:"gist"turns anaudiotask into a meeting-notes task graded on meaning, not verbatim match. The candidate captures the main points rather than transcribing exactly, so the accuracy/error stats are hidden (only Speed / Time show), and — withincludeTranscript—stats.txtalso carries aKey Points:block (fromkeyPoints) before the verbatim transcript so the grader can score coverage and verify captured facts. Default: normal verbatim grading.keyPoints: array of strings (used withgradeMode: "gist") — the reference key points/main ideas the notes should capture; emitted intostats.txtfor the grader.racing: mode-specific config (opponentSpeeds,mistakesAllowed)meteoriteRain: mode-specific config (meteoriteSpeed,spawnInterval,pointsPerChar,difficulty)towerDefense: mode-specific config (initialLives,cellSize,enemySpawnInterval,enemySpeed,enemyHealth)audio: mode-specific config (src— URL of an audio/video clip to play;rate— playback rate, e.g.0.9;maxPlays— limit the number of times the clip can be played, e.g.2for a meeting-notes task; omit for unlimited)
Text may contain multiple paragraphs: newlines in text-to-input.txt are preserved (blank lines separate paragraphs) for every mode except racing, which is a single-line track and flattens them to spaces.
markingMode: how typed text is compared with the reference on screen.positional(default) is the original behaviour — one span per reference character, right or wrong at a fixed index.characteraligns the two strings (Levenshtein), so a skipped or added character costs one character instead of shifting every position after it.wordcuts the text into runs of word characters and runs of separators (spaces and punctuation alike) and realigns those, containing a slip to the run it happened in. A dash typed where a space belongs pairs with that space, costing one substitution. The alignment modes produce two marks positional comparison cannot express: a skipped reference character (char-missing, underlined) and an added character with no reference position (char-extra, struck through).errorMetric: the same three comparisons applied to the numbers — "Errors Left (Unfixed)" and "Total Errors Made". Independent ofmarkingMode, so a task can show word alignment while scoring by character alignment. All three count in characters, so thresholds keep their meaning. Defaultpositional.
Both default to positional, so existing courses render and score exactly as
before, and an unrecognised value falls back to it with a console warning. The
difference is not small: on the 1,489-character runbook passage, one omitted
character at index 100 counts as 1 error under character, 3 under
word, and 1,358 under positional, because every position after the slip
is out of step. See client/text-metrics.js.
Backspace chords are handled by the app rather than left to the browser
(handleDeleteChord in client/input.js):
- Option/Alt+Backspace (Ctrl+Backspace on Windows/Linux) deletes the previous word — the whitespace before the caret plus the word or punctuation run in front of it.
- Cmd+Backspace deletes to the start of the visible line.
This is deliberate rather than defensive. The text modes type into
#hidden-input, which is 0 pixels wide, and with no usable line box Chrome's own
word-delete collapses to the start of the line (wiping the line instead of a
word) while its delete-to-line-start collapses to a single character. Cmd+Backspace
additionally has to measure "the line" from the rendered passage (one <span> per
character, so the row is read off the layout the user is actually looking at)
rather than from the hidden textarea, since the two wrap in completely different
places. The audio/gist transcript box is a real, visible textarea, so it keeps the
platform's own Cmd+Backspace and only borrows the word-delete chord.
In audio mode the target text is not shown. A recorded clip from audio.src is
played through the browser's native audio player — play/pause, seek, elapsed /
total time, volume, and playback speed (via its overflow menu) — and the user
transcribes what they hear. (rate sets the initial playback speed; if no src
is set it falls back to the browser's speech synthesis with simple Play/Replay
buttons.) Accuracy is a character-level similarity to the reference text
(text-to-input.txt, which must match the clip) and the errors-left metric
reports the number of mis-transcribed words (these two are relabeled "Character
errors" / "Word errors" on the audio results dashboard). When includeTranscript
is enabled, the saved stats.txt also includes both the expected and submitted
transcriptions so a grader can evaluate the actual transcription, not just the
numbers.
client/index.html: app shell and mode containersclient/typing-simulator.js: core gameplay and stats logicclient/text-metrics.js: pure text-comparison helpers shared by rendering and scoring (positional, character alignment, word alignment)client/games/: per-mode implementations (classic-game.js,racing-game.js,meteorite-rain-game.js,tower-defense-game.js,audio-game.js)client/typing-simulator.css: gameplay stylesclient/app.css: shared shell/layout stylesclient/help.js: help modal bootstrapclient/design-system/components/modal/modal.js: design-system modal used for helpclient/public/help-content.html: help text shown in the modal (copied intodist/by Vite)client/text-to-input.txt: source text used for typingserver.js:/save-stats, production static hostingextract_solution.py: parses and printsclient/stats.txt
POST /save-stats- Body: plain text payload
- Persists results to
client/stats.txt.
- Help content is loaded from
client/public/help-content.html(served as/help-content.html) and shown viaModal.createHelpModalfrom the design system when#btn-helpis clicked. - In development, Vite serves static assets and proxies
/save-statsto the API server.