Quick Start
Tharser authors text adventures for the Thumby (72×40 OLED handheld, 6 buttons). Pick a kind on the welcome screen — Choice or Parser — or import a game.json to keep editing.
Two engines
- Choice: branching narrative with 1–4 numbered choices per scene. ↑/↓/A/B pick the choice; that's it. Best for short stories with paths.
- Parser: a small world with rooms, items, exits, flags, and a scripting language. Player navigates and manipulates the world. Best for puzzle games. Has two input styles: hardware-style verb wheel (default) or typed text (browser-only).
Editor layout
- Left pane: for Choice, a scene list. For Parser, six tabs: Rooms, Items, NPCs, Img (cutscene images), Game (title/start/win + triggers), and In (typed input config).
- Center pane: form for the selected entity, plus a graph/map view.
- Right pane: live preview running the JS engine. Click the preview's d-pad or use arrow keys + Z/X (or A/B) to play.
Saving your work
The editor doesn't auto-save. Export JSON at the top to save a game.json file. You can import it back later to keep editing.
Sharing your game
Three export targets, all from the toolbar:
- .html: playable in any browser (itch.io ready).
- .py: single Python file for the Thumby IDE.
- Bundle: all three formats + README in one zip.
Choice Engine
Each game has a title, a start scene id, and a map of scenes. Each scene has text and up to 4 choices, with an optional sound effect that fires when the scene is entered.
{
"title": "Cave",
"start": "mouth",
"sounds": {
"intro": ["A4:150", "C5:150", "E5:300"],
"treasure": ["E5:80", "G5:80", "C6:200"]
},
"scenes": {
"mouth": {
"text": "You stand at the mouth of a dark cave.",
"sfx": "intro",
"choices": [
{ "label": "Enter the tunnel", "next": "tunnel" },
{ "label": "Walk to fields", "next": "field" }
]
},
...
}
}
Display
The full scene text scrolls left across the top of the OLED.
Choices are listed below with anchored button-icon prefixes
(^., v., A., B.)
that map directly to the button the player presses. The prefix
stays fixed at the left while a long label scrolls within its
row — so the player always knows which button picks which
choice, even mid-scroll. Hold → fast-forwards the
marquee at 4×.
Sound effects on scene entry
Set "sfx": "soundname" on any scene; the sound
fires when the player enters that scene. It re-fires on every
entry, so revisiting a scene plays its sound again. See the
Sound tab for details on defining sounds.
Endings
A scene with no choices is an ending. The OLED shows
"THE END · A=again" and pressing A restarts from the title.
Endings can still set sfx for a closing chime.
Limits
- Title: 12 characters fit on the title screen.
- Choice label: any length — labels that exceed the 60-pixel band scroll horizontally within their row. Authors no longer need to constrain label length for display.
- Max 4 choices per scene.
- Scene text length: unlimited; longer text just scrolls longer.
Parser Engine
A world model with rooms, items, exits, flags, and scripted item interactions. Player navigates with a verb wheel by default, or types commands when typed input is enabled.
{
"title": "Cellar",
"start_room": "kitchen",
"win_message": "Sunlight. Freedom!",
"rooms": {
"kitchen": {
"name": "Kitchen",
"desc": "Cold stone floor.",
"exits": { "down": "cellar_dark" }
},
...
},
"items": {
"torch": {
"name": "torch",
"desc": "An unlit pine torch.",
"where": "kitchen",
"takeable": true,
"verbs": {
"USE": [
"if:flag:lit=1?msg:Already burning.|msg:Flame!",
"flag:lit=1",
"if:in:cellar_dark?goto:cellar_lit"
]
}
},
...
}
}
Rooms
- name: shown on entry (12-char display limit, longer scrolls).
- desc: shown the first time the player enters.
- exits: map of direction → room id. Common directions:
north,south,east,west,up,down,in,out. Anything is allowed. An exit can also be a locked-door object — see Locked Doors below. - on_enter: optional script (effect string list) that runs every time the player enters this room.
- on_first_enter: optional script that runs only the FIRST time the player enters (introductions, one-shot events).
- image_on_enter: id of an Image to display when entering. Set image_persistent: true to show on every entry; default shows only on first entry.
Items
- name: display name (also what the player types in typed mode).
- desc: shown by EXAM.
- where: room id where the item starts, or
"inv"if it starts on the player, or"nowhere"if offstage. - takeable: false to prevent TAKE (e.g., scenery, doors).
- verbs: map of verb name (USE, TAKE, etc.) → script. Runs when the player uses that verb on this item.
- synonyms: optional array of alternate names players can type or that show up in autocomplete. e.g.
["key", "brass"]for a brass key.
Synonyms (typed input only)
To make typed input more forgiving, add a synonyms
array to items and NPCs:
"brass_key": {
"name": "brass key",
"synonyms": ["key", "brass", "keyring"],
...
}
And at the top level of the game, define verb aliases:
"verb_synonyms": {
"take": ["get", "grab", "pick"],
"use": ["operate", "try", "push", "pull"],
"exam": ["read", "check", "inspect"]
}
Now grab key resolves to TAKE brass_key. Autocomplete
predicts synonyms too: typing gr suggests
grab. The wheel UI ignores synonyms (it shows
canonical names), so the same game.json plays identically on
hardware.
Locked Doors
An exit can be a string (a room id) or an object:
"exits": {
"north": {
"room": "garden",
"locked": true,
"key": "key",
"lockedMsg": "The iron door is locked."
}
}
If locked is true, GO that direction shows
lockedMsg. If the player carries the named
key item, the door auto-unlocks on GO and they pass
through. The Rooms tab's exit editor exposes this.
Built-in verbs
GO · LOOK · TAKE ·
DROP · USE · TALK ·
GIVE · EXAM ·
INV · WAIT. The wheel only shows verbs
relevant to the current room (TAKE only when something takeable
is here, USE only when usable items exist, TALK only when there's
a chatty NPC, etc.).
start_inv
Optional array of item ids to start the player carrying.
NPCs & Dialogue
NPCs are characters the player can TALK to, GIVE items to, USE
(e.g. a sword on a dragon), and EXAM (read description). They live
in world.npcs alongside items, and they share the
verb-wheel target system.
"npcs": {
"ghost": {
"name": "ghost",
"desc": "A pale figure. Hungry-looking.",
"where": "cellar_lit",
"topics": [
{ "label": "Greet",
"script": ["msg:'You bring light at last.'"] },
{ "label": "Ask key",
"script": [
"if:has:biscuit?msg:'Food! Then the key is yours.'|msg:'Bring me something to eat.'"
] }
],
"gifts": {
"biscuit": [
"msg:The ghost takes the biscuit and smiles.",
"flag:gave_biscuit=1",
"move:key=cellar_lit",
"move:biscuit=nowhere"
]
}
}
}
Fields
- name: display label on the verb wheel.
- desc: shown by EXAM.
- where: room id (or "nowhere" if not yet present).
- synonyms: optional alternate names for typed input
(e.g.
["spirit", "phantom"]for a ghost). Wheel UI ignores. - topics: list of dialogue topics. TALK opens a sub-wheel cycling through them; the player picks one with A; that topic's script runs.
- gifts: dict mapping inventory item id → script. Runs when the player GIVEs that item. The item is automatically removed from inventory (assigned to a "_given_NPC" zone).
- verbs.USE: script that runs when the player USEs this NPC (e.g., attacking with a weapon).
- verbs.GIVE: fallback script for any gifted item not
in the
giftstable.
How TALK works
On the wheel, TALK appears when there's an NPC in the room with topics or dialogue. Pressing A opens a topic sub-wheel: ↑/↓ cycle topics, A asks the selected one, B ends the conversation. Topic scripts can reference flags, branch on state, queue images, or move other NPCs/items.
How GIVE works
The wheel's GIVE verb targets an NPC. Pressing A opens an
inventory sub-wheel: ↑/↓ cycle through your inventory,
A confirms giving the selected item to that NPC, B cancels. If
the NPC has a matching entry in their gifts dict,
that script runs; otherwise their fallback verbs.GIVE
fires; otherwise "they don't want it".
In typed input the player can write "give biscuit to ghost" (or "give biscuit ghost") in two-word or freeform grammars and skip the sub-wheel entirely. Single-word grammar can't express GIVE — that's the use case the sub-wheel exists for.
Global Triggers
Triggers fire effects when a world condition becomes true. They run after every command (verb commit, topic ask, etc.), letting you build state machines that react to player actions without binding to a specific item or room.
"triggers": [
{ "when": "flag:dragon_dead",
"do": "msg:The castle gates swing open.",
"once": true },
{ "when": "in:throne_room",
"do": "msg:The king watches you." }
]
Fields
- when: a condition (same syntax as
if:— see Effect Strings). - do: a single effect string. For multiple effects, use
msg:A / msg:Bwith " / " separators in the editor's multi-line input, or usegoto:room_with_on_enter_scriptto chain through a room's on_enter. - once: if true, the trigger fires at most once per game. Otherwise it fires every time the condition is true.
Common patterns
- One-shot intro:
oncewithvisited:room. - Time bomb: increment a counter on every turn via a non-once trigger, then a once trigger that fires when counter hits N.
- Conditional unlock: once trigger that calls
unlock:room=dirwhen a flag is set.
Images & Cutscenes
Full-screen images can be shown at any time via the
show: effect or attached to a room as
image_on_enter. Three kinds:
Pixel art (72×40)
Edit in the Art tab's pixel canvas, scaled 4× for clarity. Draw, erase, clear, invert. Stored as a 360-byte payload (base64) in game.json — about 480 base64 characters per image. The 8-pixel grid overlay helps with rough composition.
Composition rules at 72×40:
- Silhouette over detail. At 1 bit, what reads is the outline. Spend pixels on a clean recognizable shape first; interior detail second.
- Two-pixel lines for emphasis. Single-pixel lines disappear next to text. Use 2-pixel strokes for things you want noticed.
- Compose in 8×8 cells. The grid marks the font's character cells. Aligning art to those gridlines harmonizes with text overlays.
- Dithering as gradient. No grays exist; fake them with checkerboard patterns. Use sparingly — it reads as texture, not depth.
- Negative space is a tool. An all-black canvas with one ON cluster commands attention.
Storage: Each row of 72 pixels packs into 9 bytes (MSB-first). Forty rows × 9 bytes = 360 bytes per image. The editor encodes/decodes automatically; you only see the base64 string in the JSON.
Tools: Draw paints ON pixels. Erase paints OFF. Clear All wipes the canvas. Invert flips every pixel. Paste b64 imports a pre-encoded 480-character base64 string (e.g., from another Thumby sprite tool).
Text scene card
Up to 5 lines × 12 characters, rendered with the built-in font, centered vertically. Good for chapter titles, "YOU WIN" cards, or quick state markers. Empty lines work as spacing.
Marquee (scrolling banner)
For long titles or messages that don't fit in 12-char lines. The text scrolls horizontally across the middle of the screen. Optional pinned captions above and below:
"title_card": {
"type": "marquee",
"text": "AN ADVENTURE IN A VERY SMALL PLACE",
"caption_top": " 6:02 AM",
"caption_bottom": "A=continue"
}
The marquee scrolls 1 px/frame normally, 4 px/frame while the
R button (right d-pad) is held. The image waits for an A press
to dismiss regardless of any :N auto-hold — players
need the full read pass.
Showing an image
Three ways:
- From a script:
show:img_idqueues the image. Add:Nto auto-dismiss after N frames (e.g.show:flash:30). Without N, the player presses A to dismiss. Marquee-type images always wait for A. - From a room: set
image_on_enterin the room's editor. By default this shows only on first entry; check "Show every entry" for it to persist. - From a trigger: a global trigger's
docan useshow:just like any other effect.
While an image displays, the verb wheel and keyboard are hidden. A or B dismisses, then play resumes.
Long titles
The game's title field is shown on the intro
screen. Short titles (≤12 chars) display statically and auto-
advance after ~1.5 seconds. Long titles scroll as a marquee and
wait for an A press, so the player can read the whole thing.
Effect Strings
Scripts attached to items (via verbs) and rooms
(via on_enter) use a tiny declarative DSL. Each entry
is a single string command. Lists run in order.
Commands
msg:TEXT | Queue a message to display. |
move:ITEM=DEST | Move an item. DEST is a room id, inv, or nowhere. |
move_npc:NPC=DEST | Move an NPC. DEST is a room id or "nowhere". |
flag:NAME=VAL | Set a flag (number if numeric, else string). |
flag:NAME | Set a flag to 1 (truthy). |
goto:ROOM | Move the player to a room. |
show:IMG | Display image full-screen; A dismisses. |
show:IMG:N | Display image for N frames, then auto-dismiss. |
unlock:DIR | Unlock an exit in the current room. |
unlock:ROOM=DIR | Unlock an exit in a specific room. |
lock:DIR / lock:ROOM=DIR | Lock an exit (without changing key). |
win | End the game with the win message. |
lose:TEXT | End the game with a custom message. |
if:COND?THEN|ELSE | Conditional. ELSE is optional. THEN/ELSE is a single effect string each (can be another if:). |
Conditions
flag:NAME | True if the flag is truthy. |
flag:NAME=VAL | True if the flag equals VAL (string compare). |
has:ITEM | True if the player carries the item. |
in:ROOM | True if the player is in the named room. |
npc:NPC | True if that NPC is in the current room. |
visited:ROOM | True if the player has ever entered that room. |
!COND | Negation. !flag:done is true when done is unset. |
A && B && ... | Short-circuit AND. has:key && !flag:used. |
Example: a torch that lights a dark cellar
"verbs": {
"USE": [
"if:flag:lit=1?msg:Already burning.|msg:The torch flares to life.",
"flag:lit=1",
"if:in:cellar_dark?goto:cellar_lit"
]
}
Reads: if the torch is already lit, just say so; otherwise announce the flare. Always set the flag to 1. If currently in the dark cellar room, transport the player to the lit version.
Example: combined AND condition with negation
"USE": [ "if:!has:office_key?msg:The reset cover is locked. You need a key.", "if:has:office_key && !flag:all_failed?msg:Not yet. More to try first.", "if:has:office_key && flag:all_failed?flag:try_print=1" ]
Three independent lines, each fully gated by its own condition.
Use && to express "all of these must be true"
and ! for negation. This pattern is much cleaner than
nesting if:s — the engine evaluates each line
independently, so a condition that's false simply emits nothing.
Typed Input (Parser, Browser Only)
Setting the Input tab to "Typed Text" turns on letter-by-letter typed entry in the browser player. The Python hardware engine ignores this setting and always uses the wheel.
Schemes
- wheel: ↑/↓ cycles through A-Z + space + submit. A commits the current letter to the buffer or submits if on the submit marker. B backspaces. Authentic and predictable.
- wheel_predict (recommended): same as wheel, plus R accepts the auto-completed word the engine suggests from the game's vocabulary. Most common typed mode.
- qwerty: on-screen 10×3 keyboard. D-pad moves a cursor; A presses a cell. Includes space, submit, and backspace cells. See QWERTY layout below for details.
QWERTY layout
The on-screen keyboard is a fixed 10-column × 3-row grid:
Row 1: Q W E R T Y U I O P Row 2: A S D F G H J K L _ (last cell is space) Row 3: Z X C V B N M . > < (> = submit, < = backspace)
Total: 26 letters + space + period + submit + backspace = 30 cells.
The cursor highlights one cell at a time and wraps in both axes.
↑/↓ moves row; ←/→ moves column. A presses the cell. B is a
backspace shortcut from anywhere — faster than navigating to
<.
In the browser, physical-keyboard typing works in ALL three schemes — wheel, predictive wheel, and qwerty. Letter keys, digits, and Space append to the parser buffer immediately; Enter submits; Backspace deletes. The on-screen UI (wheel or QWERTY grid) keeps working in parallel as a touch-friendly alternative. On hardware Thumby there is no physical keyboard, so the on-screen UI is the only input source — the same game.json plays correctly on both targets.
Grammars
- two_word (default): classic IF.
take torch,go down,use key. Bare directions (down) auto-expand togo down. - single_word: player types one word; engine infers
verb.
torch→ take torch (if in room) or use torch (if in inventory).down→ go down. Faster typing, less expressive. - freeform: engine scans the player's whole buffer for
any vocabulary word.
pick up the torch,i want to go downall work. Forgiving of fillers.
Vocabulary
The engine knows: built-in verbs (go/look/take/drop/use/exam/
inv/wait), single-letter aliases (n/s/e/w/u/d for directions, i
for inventory, l for look, x for examine), exit directions of the
current room, item and NPC names in the room or inventory, plus
any author-defined synonyms from data.verb_synonyms
and per-item/NPC synonyms arrays. Predictions and
recognition draw from this set; nothing else matters.
Synonyms
Authors can make typed input dramatically more forgiving by
defining synonyms. See the Parser tab for the syntax. Synonyms
flow through the same parser and prediction layers as canonical
names — grab key works exactly like take brass_key.
Long buffers
The input line shows your current command. If it overflows the 12-character screen width, the engine shows the tail (with a left-edge marker) while you're typing, then scrolls the whole thing as a marquee when you pause. So you can always see what you've typed.
Errors
If the parser can't understand your command, the error flows
through the main response marquee (full length, scrollable) the
same way any room or item response does. The message echoes the
unknown word: I don't know 'wibble'.
Unrecognized prefix indicator
While typing, if your current prefix matches no vocabulary
word, a small * appears in the corner. You can still
submit, but it warns that nothing useful matches.
Exports
All four export buttons work for both Choice and Parser games.
| JSON | Just the game data. Use this to save in-progress work; you can import it back to keep editing. |
| .py | Single Python file with the game data embedded
in the engine source. Drop on a Thumby in /Games/<name>/
and it runs. Easiest hardware deployment. |
| .html | Self-contained browser-playable page. ~65 KB. Upload to itch.io as an HTML game; no special flags or headers required. Includes the three retro screen tints (green / amber / crisp) switchable from a toolbar at the top of the page. |
| Bundle | All three of the above zipped together, plus a short README. Best for sharing — recipients get the editable JSON, the hardware-ready .py, and the browser-playable .html in one download. |
The .html packager
The .html and Bundle exports use the same wrapping logic the original standalone packager did. You can import any game.json (yours or someone else's) and click .html or Bundle to re-package it — there's no separate packager tool.
File names
Files are named after your game's title, sanitized for safety.
A title of "My Cool Game!" becomes My_Cool_Game_.json.
Rename freely after download.
Sound Effects
Tharser games support author-defined sound effects on both targets — Web Audio in the browser, the Thumby's piezo buzzer on hardware. Sounds are short monophonic note sequences, in the spirit of the genre's heritage (think original Manananoggin on the Apple II or any 80s-era IF jingle).
Defining a sound
In the Sfx tab, give each sound a name and a sequence
of NOTE:duration_ms strings:
"sounds": {
"intro": ["G#4:350", "F#4:350", "E4:350", "B3:700"],
"pickup": ["A5:50", "C6:80"],
"win": ["C5:120", "E5:120", "G5:120", "C6:300"]
}
Notes use scientific pitch notation: C through
B with optional # or b,
followed by an octave number (2 through 7). Durations are in
milliseconds. Use rest:200 or -:200
for silence.
Triggering sounds from gameplay
Parser games: anywhere an effect string is accepted
(verb scripts, NPC dialogue, triggers, room on_enter
or on_first_enter), use sfx:NAME:
"on_first_enter": ["sfx:intro"] "script": ["msg:You take it.", "sfx:pickup"]
Choice games: set sfx on a scene, and
the sound fires every time the player enters that scene:
"scenes": {
"mouth": { "text": "...", "sfx": "intro", "choices": [...] }
}
Playback rules
Tharser is monophonic — only one sound plays at a time. Triggering a new sound cancels any in-progress sound. That's intentional and matches the hardware's single-channel piezo; if you want layered audio you'd need a different platform.
The browser uses a square-wave oscillator with a short attack/release envelope to mimic the piezo's character without the harsh on/off clicks. On hardware the actual piezo is used directly. The same game.json sounds identical on both, modulo speaker quality.
When sounds play (or don't)
Sounds queued during the studio splash / title screens wait until the player dismisses the title with A. This is intentional: the player can't hear anything yet (browser autoplay policy requires a user gesture before audio is unblocked), and an intro chime that fired during the splash would be inaudible. The chime plays the moment gameplay begins.
The mute toggle (♪ / 🔇) in any standalone HTML, or the M key, silences output but does not stop the engine from queueing or advancing sounds.
Keyboard & Preview Controls
The preview pane mirrors the hardware. Click the on-screen buttons or use your keyboard. Browser players using your standalone .html files get the same controls.
D-pad and action buttons
| ↑ / ↓ / ← / → | D-pad |
| Z / Enter / Space | A button |
| X / Backspace / Esc | B button |
| M | Mute / unmute audio (standalone HTMLs) |
Physical-keyboard typing
In any parser scheme — letter wheel, predictive wheel, or QWERTY — browser players can type commands directly on a physical keyboard. Letters, digits, and Space append to the parser buffer; Enter submits; Backspace deletes. The on-screen wheel/QWERTY UI keeps working in parallel as a touch-friendly alternative.
On hardware Thumby there is no physical keyboard, so the on-screen UI is the only input source. The same game.json works correctly on both targets — typing is a browser-only convenience that hardware simply doesn't see.
Hold-to-fast-forward
Hold → (right d-pad) while text is scrolling to speed it up to 4× and shrink the end-of-message pause. Release to resume normal speed. Once text completes, the right d-pad goes back to its normal function (cycle wheel target / accept autocomplete / move QWERTY cursor right). Choice games also support hold-right for fast marquee.
Mute toggle
The ♪ button in the top-right of any standalone player (and the M keyboard shortcut) toggles mute. The icon switches to 🔇 when muted so it's obvious the toggle worked. Mute state persists for the session but resets on page reload.
Hardware Deployment
Thumby is an RP2040-based handheld running MicroPython. To put a Tharser game on one:
Single-file path (recommended)
- In Tharser, click .py to export.
- In the Thumby IDE, create a new folder under
/Games/(any name). - Upload the .py file as the folder's main script.
- Done. The game data is baked into the .py.
Bundle path (sharing)
- Click Bundle to download all three formats zipped together.
- Send the zip to whoever you're sharing with. They can:
open the
.htmlin any browser, drop the.pyonto a Thumby, or open the.jsonback in Tharser to keep editing.
Browser-only features
Typed input (Input tab) is JS-engine-only. On hardware, parser
games always use the verb wheel. The game.json's input
field is simply ignored by the Python engine, so the same game
file plays correctly on both targets.
About Tharser
Tharser is a play on Thumby + parser. The project produces the world's smallest parser text adventure games — rendered onto a 72×40 monochrome framebuffer, playable on the Thumby handheld or emulated identically in any browser.
- A unified browser editor for Choice and Parser games.
- Two Python engines that run on Thumby hardware.
- A JavaScript port that runs in browsers, with three letter- input schemes and three grammar modes for typed parser games.
- Self-contained .html packaging for itch.io and similar.
Author
Tharser and every bundled example game are the sole work of Tyler Wright, DET, an indie game developer publishing as Outgrabe (outgrabe.itch.io). Tyler holds a Doctor of Educational Technology from Central Michigan University, with research interests in game development, interactive fiction, and game engines. Prior parser-IF work includes the ParserComp 2024 entry Moon-house Technician, written originally in REXX. The hardware medium — the Thumby — is the product of TinyCircuits (Akron, Ohio).
Inspiration
Tharser is dedicated to Teeny Ted from Turnip Town (2007) by Malcolm Douglas Chaplin (writer) and Robert Chaplin (publisher, engraver). At 0.07 × 0.10 millimeters, etched on silicon with a gallium ion beam at Simon Fraser University's Nano Imaging Lab, it is Guinness-certified as the world's smallest reproduction of a printed book. Tharser borrows its central question: why does it exist at all? — and answers it with the same defiant shrug. Making things at impossible scales is one of the oldest and best human pleasures.
Browser vs. hardware
The browser player emulates the Thumby. It runs the same game.json with a JS port of the engine on a 72×40 framebuffer, rendered scaled into a canvas. Every behavior matches hardware: font, scrolling speed, wheel cycling, image rendering, all of it. The difference is the input — browser players use keyboard or on-screen buttons in place of physical d-pad/A/B, and Parser games can optionally use a typed-text input mode that hardware doesn't have.
Game data format
All games are JSON. The same file works on hardware (via the Python engines) and in browsers (via the JS engine). You can hand- edit the JSON; the editor is just a convenience.
Recent additions
- Bundled examples: click Examples in the toolbar (or on the welcome screen) to load a built-in game. Ships with Cave (Choice), Closet (Parser tutorial), and Cellar (mid-size Parser with an NPC, gifts, and a locked door).
- Compound conditions:
&&(AND) and!(NOT) inif:scripts. - Synonyms:
data.verb_synonymsfor verb aliases and per-entitysynonyms: [...]arrays. - Marquee image type: long-form scrolling banners with optional captions.
- Long-text fit-or-scroll everywhere: verb-wheel targets, give-wheel items, topic labels, typed buffer, parser errors all marquee when they overflow instead of being silently truncated.
Adding new verbs or effects
The verb set is fixed at the engine level (GO/LOOK/TAKE/DROP/ USE/TALK/GIVE/EXAM/INV/WAIT). Items can define handlers for any of these. Effect strings are the way to script behavior; adding new effects requires changing the engine source.