Skip to content

Standalone HTML player

Export → Standalone HTML player produces a single .html file with the scenario JSON, a player runtime and its UI all inlined. No server, no external requests, no assets; it runs from file://, from an email attachment, from any static host. It records the path taken and shows a summary at the end.

example-scenario-player.html is the example project as a player — open it and view source.

  • Playtest builds before the engine work exists.
  • Review links — send a stakeholder one file.
  • QA harness — a deterministic reference for “is it the content or the integration?” (see Building a custom runtime).
  • The base of the SCORM and xAPI packages.

Everything in the runtime semantics: group nodes, priority suppression, passthrough, [f]-forced menus, DelaySimStatus, | sentence splits, [var=…] substitution, [?Var] input prompts (rendered as a text field), conditions and scripts in the Lua subset (see Lua), cross-conversation links, and localised text for the language chosen at export.

Deliberately not included:

  • Media. No audio, video or pictures — they would make the file large and break offline portability. The player is text.
  • Full Lua. A WebAssembly Lua would be ~400 KB of base64 and would not load from file:// on some browsers. Scripts outside the subset are reported in the export’s warnings and are no-ops in the player.
  • Persistence. No localStorage: a training file that leaves state on a shared machine is a support ticket, and SCORM owns persistence anyway. Reloading the page restarts the run.

The player runtime is a port of the editor’s simulator, and it is kept faithful by a test rather than by shared source: every commit runs a battery of scenarios through both and asserts byte-identical transcripts and final variable state. If the editor plays it one way, the file plays it the same way.

The page exposes window.chatmapperPlayer. A wrapper that loads before the player (a <script> in <head>, which is how the SCORM wrapper is injected) may pre-create the object; the runtime merges into it rather than replacing it.

interface ChatMapperPlayer {
version: 1;
scenario: Scenario; // the embedded canonical JSON
session: Session; // the live traversal
summary: Summary | null; // null until the run ends
onComplete: ((s: Summary) => void) | null; // assignable; called once when the run ends
onChoice: ((c: Choice) => void) | null; // assignable; called as each choice is taken
whenComplete(fn: (s: Summary) => void): void; // like onComplete, but fires immediately if already ended
beforeComplete?: (s: Summary, done: () => void) => void; // optional gate: call done() to release onComplete
restart(): void; // new session; summary back to null
}
interface Session {
start(): State;
choose(index: number): State; // index into state.choices
submitInput(values: Record<string, string | number>): State; // answer a [?Var] prompt
summary(): Summary;
state: State | null;
}
interface Summary {
completed: boolean; // the run reached an end of conversation
nodesVisited: number; // unique playable nodes entered
totalNodes: number; // playable nodes in the whole project
visited: { convId: number; nodeId: number }[];
endings: { convId: number; nodeId: number }[]; // nodes that had no successor
endingsReached: number;
choices: { convId: number; nodeId: number; text: string }[]; // in the order taken
variables: Record<string, unknown>; // final state, keyed by cleaned Lua name
}

Log the path a tester took:

<script>
window.chatmapperPlayer = window.chatmapperPlayer || {};
window.chatmapperPlayer.onChoice = c => console.log('chose', c.text, 'at', c.convId + ':' + c.nodeId);
</script>

Post the result somewhere when the run ends:

window.chatmapperPlayer.whenComplete(summary => {
fetch('/playtests', { method: 'POST', body: JSON.stringify(summary) });
});

Drive the player headlessly (no UI) — the runtime and the UI are separate scripts, and the runtime touches no DOM:

const s = window.chatmapperPlayer.session;
let st = s.start();
while (!st.ended) {
if (st.pendingInput) st = s.submitInput({ PlayerName: 'Ines' });
else st = s.choose(0); // always the first option
}
console.log(s.summary());

The export has two options beyond the defaults: the starting conversation (defaults to the first) and the display language. The SCORM and xAPI packages add head/tail scripts around the same file.