A website for solving classical ciphers by hand, with assistance. The guiding principle: remove the grunt work, never the insight. It is deliberately not an auto-solver; the user should finish feeling they solved it themselves.
Project brief: docs/project-brief.md (snapshot; the Google Doc is the source of truth).
Early prototyping. The first cipher family is monoalphabetic substitution, and the current work is choosing how the substitution screen looks and behaves.
No build step. Open prototypes/index.html in a browser (works from file://), or serve the folder:
python3 -m http.server -d prototypes 8000| Prototype | Key layout | Default display |
|---|---|---|
p1-ledger.html |
Horizontal key band: one column per cipher letter, frequency bar above, guess slot and notes below | Interlinear |
p2-ladder.html |
Vertical key ranked by frequency in a sidebar, plus repeated pairs, triples, doubles and word patterns | Side by side |
p3-two-way.html |
Key in both directions (cipher→plain and plain→cipher); assign from either side | In-place |
p4-focus.html |
Text-first; an inspector for the selected letter (candidate board, contexts) and a compact key strip | In-place |
All four share the same behaviour, so they can be compared on layout alone:
- Guessing: type a letter against the selected cipher letter, in the text or in the key. The guess propagates everywhere.
- Pencil and ink: new guesses are pencil (grey italic).
Enterinks one (dark upright) to mark it settled. - Notes:
Shift+letter records a candidate without propagating it. Clicking a note swaps it in; the old guess becomes a note. - Clashes: two cipher letters guessed as the same plaintext letter are marked with a wavy vermilion underline. This is bookkeeping, not a correctness signal.
- Undo: per-letter clear (
Backspace),Ctrl+Z/Ctrl+Shift+Z, and a History panel with a scrubber over every step. - Layout: words kept, or groups of five for the built-in puzzles.
- Display: interlinear, in-place, side by side, stacked (keys
1–4). - Check: a counted yes/no check against a hash of the plaintext, available once every cipher letter has a guess.
Progress is saved per prototype and puzzle in the browser's localStorage.
docs/
project-brief.md snapshot of the project brief
prototypes/
index.html gallery of the four prototypes
p1-ledger.html … one page per prototype (layout + key grid only)
shared/
core.js substrate: tokenising, stats, Solver (state, history, clashes), check
app.js app shell: toolbar, text rendering, keyboard model, history, check UI
base.css design tokens (light/dark) and shared components
hash.js plaintext hash (cyrb53; prototype only)
puzzles.js GENERATED: ciphertext + plaintext hash
tools/
make-puzzles.cjs builds shared/puzzles.js from tools/sources/*.txt
sources/ plaintext sources (spoilers!)
Regenerate puzzles after editing sources: node tools/make-puzzles.cjs.
- The check can't be partial. With only a hash of the full plaintext, the workbench can say whether a complete mapping is right, but not whether "everything so far" is. A per-letter hash would leak which letters are wrong. The prototypes therefore require every cipher letter to have a guess before checking, which also works as friction.
- Reference frequencies are a nudge. Placing the English frequency order beside the ranked cipher letters (P2) or comparing a guess's expected frequency with its observed one (P3) edges toward "suggesting what a letter should be". Both are off by default behind a toggle so they can be judged in use.
- Clash direction. Clashes are defined on the plaintext side (two cipher letters → one plaintext letter). Re-guessing a cipher letter just replaces its guess, so the other direction never arises.
- Pencil vs ink is purely the user's own bookkeeping. Nothing is locked; ink can be reverted, and "Erase pencil" clears every unsettled guess at once.