Mishap22 Developer's Guide

A stand-alone implementation guide for the Mishap22 text-adventure runtime: screen rendering, input parsing, timing, save/load behavior, storage modes, and safe patching conventions.

Project: Mishap22 Runtime: Browser JavaScript Server: PHP endpoints Storage: Local + Server Document: Stand-alone HTML

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.

Core invariant: only actual gameplay commands should advance elapsed time or the turn counter. Help, loading, saving, opening panels, closing panels, and storage selection should not tick the game.

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.
Preserve archival paths when repacking patches. Changed-only zips should include only modified files, but each file must keep its original relative path.

3. Boot Flow

  1. The PHP entry point emits the root document, screen container, and script/style tags.
  2. The browser loads CSS first so the visual frame exists before the engine paints.
  3. The engine fetches glyph CSV files.
  4. CSV glyphs are parsed into renderable bitmap data.
  5. The runtime initializes state without starting gameplay time.
  6. The help or splash display is rendered.
  7. The screen performs a first-fit layout pass after glyphs are ready.
  8. 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 guest or 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();
}
Save/load should serialize the game state exactly as it exists at the moment of save, including elapsed gameplay time and turn count, but not transient UI-only details unless they are intentionally restorable.

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."
}
Do not allow the browser to send an absolute filesystem path for server save/load. Resolve paths server-side from login data and sanitized slot names.

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
  }
}