1. Overview
Mishap22 is a browser-hosted text adventure engine built around a fixed visual screen, bitmap-style glyph rendering, command input, parser feedback, and persistent game state. Mishap22 is the engine layer; specific games and content can run on top of it without changing the runtime contract.
The guiding rule is that display state, command state, and save state must remain synchronized. The screen should resize and scroll correctly, commands should produce deterministic parser results, and save/load should never advance gameplay time.
2. File Map
Exact filenames may vary between snapshots, but the current Miser/Mishap22 layout usually follows this shape:
| Path | Purpose |
|---|---|
/public/official/Mishap22/index.php |
Page entry point. Loads CSS, JavaScript, glyph CSVs, and runtime container markup. |
/public/official/Mishap22/js/Mishap22.js |
Main engine runtime: screen, parser, input, game state, clock, help, save/load UI. |
/public/official/Mishap22/js/Levenshtein.js |
String similarity helper used for guess/nearest-command feedback. |
/public/official/Mishap22/css/Mishap22.css |
Layout, scaling, screen frame, HUD, panel, and input styling. |
/public/official/Mishap22/csv/FONT.CSV |
Bitmap font glyph source. |
/public/official/Mishap22/csv/SYMB.CSV |
Symbol glyph source. |
/public/official/Mishap22/MH-Save22.php |
Server-side save endpoint. |
/public/official/Mishap22/MH-Load22.php |
Server-side load endpoint. |
3. Boot Flow
- The PHP entry point emits the root document, screen container, and script/style tags.
- The browser loads CSS first so the visual frame exists before the engine paints.
- The engine fetches glyph CSV files.
- CSV glyphs are parsed into renderable bitmap data.
- The runtime initializes state without starting gameplay time.
- The help or splash display is rendered.
- The screen performs a first-fit layout pass after glyphs are ready.
- Input listeners are attached.
DOMContentLoaded
└─ new Mishap22()
├─ loadGlyphs()
├─ initializeState()
├─ renderInitialScreen()
├─ fitScreenToViewport()
└─ bindInput()
The initial screen must not be treated as a gameplay turn. The timer remains idle until the first valid gameplay command is accepted.
4. Runtime State
Keep state grouped by responsibility. Avoid one-off global variables unless they represent immutable constants.
Display State
- Current buffer text
- Scroll position
- Screen scale
- Glyph metrics
- Help/game/save panel mode
Gameplay State
- Current room/page
- Inventory or flags
- Turn counter
- Elapsed gameplay milliseconds
- Command history
Input State
- Current input buffer
- Last submitted command
- Parser result
- Levenshtein guess
- Input lock/freeze flags
Persistence State
- Storage mode: local or server
- Autosave slot/path
- Logged-in availability
- Last save timestamp
- Load result status
Recommended State Shape
this.state = {
mode: "help", // help | game | saveLoad
gameStarted: false,
frozen: false,
timing: {
startedAt: null,
elapsedMs: 0,
turns: 0
},
storage: {
mode: "local", // local | server
autosave: "autosave",
serverAvailable: false
},
parser: {
lastInput: "",
normalizedInput: "",
result: null,
guess: null
}
};
5. Screen System
The screen is responsible for rendering text in the engine’s visual style and fitting that rendering into available viewport space. It should be resilient to first-load timing, font/glyph loading, browser resize, and mobile viewport changes.
Best-Fit Rules
- Compute the available viewport width and height after CSS has settled.
- Measure the actual rendered buffer, not the theoretical character count alone.
- Scale down only as much as needed to fit snugly.
- After resize, rerender and re-scroll using the current mode’s scroll rule.
- Avoid stale cached dimensions after glyph CSV reloads.
Scroll Rules
| Mode | Scroll Behavior |
|---|---|
help |
On first load, show unscrolled/top-aligned help content. |
game |
Auto-scroll to best-fit snug against the bottom of the buffer after output. |
saveLoad |
Preserve panel visibility; do not advance or autoscroll as a command result. |
fitAndScroll() {
this.fitScreenToViewport();
if (this.state.mode === "help") {
this.scrollToTop();
return;
}
if (this.state.mode === "game") {
this.scrollToBottomSnug();
}
}
6. Input Pipeline
Input should be normalized before parsing, but the raw command should still be available for display, history, and debugging.
submitInput(raw) {
const input = String(raw ?? "");
const normalized = this.normalizeCommand(input);
if (!normalized) {
this.echoBlankInput();
return;
}
if (this.state.mode !== "game") {
this.handleNonGameplayInput(normalized);
return;
}
const result = this.parseCommand(normalized);
this.applyParserResult(result);
}
Input Rules
- A blank command should not advance the game.
- A lone invalid verb should produce an error message.
- A recognized non-game command, such as opening help, should not count as a game turn.
- Only accepted gameplay commands should start the timer.
- Input should be locked while save/load requests are actively resolving.
7. Parser + Levenshtein
The parser receives normalized input and returns a structured result. Do not rely on display text as the source of truth for parser state.
Parser Result Shape
{
ok: false,
type: "invalidVerb",
input: "nroth",
normalizedInput: "nroth",
message: "I don't know how to 'nroth'.",
levenshtein: {
guess: "north",
score: 83.33333333333334
},
advancesTurn: false
}
Levenshtein Rules
- Echo the Levenshtein result, not the originally provided input, when showing a guess.
- Return scores as a 0% to 100% float with maximal useful precision.
- Use early-exit when the best possible score can no longer reach a target percentile.
- Never allow a typo suggestion to execute automatically unless explicitly designed that way.
if (!result.ok && result.levenshtein?.guess) {
this.writeLine(`Did you mean "${result.levenshtein.guess}"?`);
}
8. Clock + Turn Counter
The clock is gameplay time, not page-open time. It should begin only after the first accepted gameplay command and should pause any time the game is not in active gameplay.
Clock States
| State | Timer Ticks? | Turns Advance? |
|---|---|---|
| Initial load | No | No |
| Help screen | No | No |
| Save/load panel open | No | No |
| Loading a save | No | No |
| Accepted gameplay command | Yes | Yes |
| Invalid gameplay command | Project choice, but usually no | Project choice, but usually no |
advanceGameplayTurn() {
if (this.state.frozen || this.state.mode !== "game") {
return;
}
if (!this.state.gameStarted) {
this.state.gameStarted = true;
this.state.timing.startedAt = performance.now();
}
this.state.timing.turns++;
}
9. Help Screen Rules
Help is a display mode, not gameplay. Returning to help should not restart the clock, and reloading back to the help screen should not accidentally begin timing.
- Entering help freezes gameplay timing.
- Leaving help resumes timing only if gameplay had already started.
- Reloading to help should preserve a clean idle clock until a gameplay command occurs.
- Help first load should scroll to the top edge of the buffer.
showHelp() {
this.freezeRuntime("help");
this.state.mode = "help";
this.renderHelp();
this.fitScreenToViewport();
this.scrollToTop();
}
10. Save / Load
Save/load is a modal runtime state. While the panel is open, the game clock and turn counter are temporarily frozen. The player may choose between local and server storage when server storage is available.
Required Behavior
- Opening the save/load interface freezes clock and turn counter.
- Closing the interface resumes only the previous active gameplay state.
- Local storage mode must always be available.
- Server storage mode is unavailable when not logged in.
- Server save/load should not be attempted as
guestor without a username. - Loading a save should restore timing values without adding elapsed time for the load operation.
openSaveLoad() {
this.freezeRuntime("saveLoad");
this.state.previousMode = this.state.mode;
this.state.mode = "saveLoad";
this.renderSaveLoadPanel();
}
closeSaveLoad() {
this.state.mode = this.state.previousMode || "game";
this.unfreezeRuntime();
this.renderCurrentMode();
}
11. Storage Paths
Mishap22 supports two storage modes: client-side local storage and authenticated server-side storage.
| Mode | Default Path | Availability |
|---|---|---|
local |
Mishap22::local::<autosave> |
Always available in supported browsers. |
server |
/pzlm-private/users/<ink::login::username>/Mishap22/<autosave> |
Only available for a real logged-in user. |
Storage Mode Gate
canUseServerStorage() {
const login = this.config?.ink?.login || {};
const username = String(login.username || "").trim();
return Boolean(
login.loggedIn === true &&
username &&
username.toLowerCase() !== "guest"
);
}
Local Save Example
saveLocal(slot, payload) {
const key = `Mishap22::local::${slot || "autosave"}`;
localStorage.setItem(key, JSON.stringify(payload));
}
Server Save Example
async saveServer(slot, payload) {
if (!this.canUseServerStorage()) {
throw new Error("Server storage is unavailable while not logged in.");
}
const response = await fetch("MH-Save22.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slot, payload })
});
return await response.json();
}
12. Server API
The PHP save/load endpoints should be strict, small, and predictable. They should not trust client-provided filesystem paths. The client can provide a slot name, but the server should resolve the final path from authenticated login context.
Endpoint Responsibilities
| Endpoint | Responsibilities |
|---|---|
MH-Save22.php |
Authenticate user, validate slot, build private path, create directory, write JSON atomically. |
MH-Load22.php |
Authenticate user, validate slot, build private path, read JSON, return structured status. |
Recommended JSON Response
{
"ok": true,
"mode": "server",
"slot": "autosave",
"path": "/pzlm-private/users/tim/Mishap22/autosave",
"payload": { "...": "..." },
"message": "Loaded."
}
Error Response
{
"ok": false,
"error": "not_logged_in",
"message": "Server storage is unavailable while not logged in."
}
13. Debugging
Common Symptoms
| Symptom | Likely Cause | Fix Direction |
|---|---|---|
| Clock starts on help screen. | Timer initialized during boot or help render. | Start timer only inside accepted gameplay command path. |
| Help does not fit on first load. | Fit pass occurs before glyph dimensions settle. | Run fit after glyph load and again on next animation frame. |
| Screen fails to scroll. | Scroll applied before render height is updated. | Render, measure, then scroll. |
| Lone invalid verb has no error. | Parser drops one-word unknown commands. | Return explicit invalidVerb result. |
| Levenshtein guess displays wrong text. | UI echoes original input instead of guess result. | Display result.levenshtein.guess. |
| Server storage appears when logged out. | Missing login gate or guest treated as user. | Disable server option unless real username is present. |
Useful Console Probes
// Check mode and timer state.
console.log(mishap.state.mode, mishap.state.frozen, mishap.state.timing);
// Check server storage gate.
console.log(mishap.canUseServerStorage());
// Check current render dimensions.
console.log({
viewport: [window.innerWidth, window.innerHeight],
screen: mishap.screenElement?.getBoundingClientRect()
});
14. Patching Rules
Mishap22 patches should be surgical, testable, and repacked safely.
- Review the current source before editing.
- Debug the actual failing path, not a guessed rewrite.
- Find the moment of clarity: the smallest cause that explains the symptom.
- Update only files that actually changed.
- Do not include stubs.
- Preserve archival paths in any zip.
- Do not silently change unrelated architecture.
Changed-Only Repack Shape
pzlm.4.test_YYYY-MMDD_MISHAP22_FIX_CHANGED_ONLY.zip
└─ public/
└─ official/
└─ Mishap22/
├─ js/
│ └─ Mishap22.js
└─ css/
└─ Mishap22.css
15. Checklists
Before Commit
- Page boots without console errors.
- Glyph CSVs load and parse correctly.
- Help first load is top-aligned and does not tick time.
- Gameplay screen auto-fits after page load and resize.
- Gameplay output scrolls snugly to the bottom.
- First real gameplay command starts the timer.
- Save/load panel freezes clock and turns.
- Local save/load round-trips successfully.
- Server storage is hidden or disabled when logged out.
- Loaded state does not add phantom elapsed time.
Parser Test Cases
"" → blank input, no turn
"help" → help mode, no turn
"north" → valid command, turn advances
"nroth" → invalid / suggested "north", no automatic execution
"xyzzy" → lone invalid verb error message
"save" → save/load mode, no turn
"load" → save/load mode, no turn
Save Payload Minimum
{
"version": 1,
"savedAt": "2026-05-14T00:00:00-07:00",
"game": {
"room": "Start",
"flags": {},
"inventory": []
},
"timing": {
"elapsedMs": 0,
"turns": 0,
"gameStarted": false
}
}