Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Closing Time — password codec

Packing a 5×5 Lights Out board into six characters a person can read aloud, write on paper, and type back in — on a handheld with no network, no keyboard and no link cable.

Extracted from Closing Time, a Game Boy Advance puzzle game.

grid 0x000ABCDE  ->  "VBSEFD"  ->  grid 0x000ABCDE

The problem

The game has a stage editor. Building a board is only interesting if you can give it to someone else, and the target hardware is a GBA — so the transport is a human. Someone reads six characters across a room, or writes them in the margin of a notebook, and types them in a week later.

That rules out anything long, anything case-sensitive, and anything containing symbols that look like each other on a 240×160 screen.

Constraints

Payload 25 bits — one per cell of a 5×5 grid
Length 6 characters. Seven is noticeably worse to transcribe
Alphabet uppercase + digits only, no lookalike pairs
Validation must reject typos rather than load the wrong board
Budget no allocation, no tables, runs on a 16.78 MHz ARM7

Six characters over a 32-symbol alphabet is 30 bits. The grid needs 25. That leaves exactly 5 bits for a checksum, which sets the ceiling on how good the error detection can possibly be: a 5-bit check can never reject more than 31 of every 32 bad inputs.

The layout

        6 symbols x 5 bits  =  30 bits

        ┌────────────────────────┬──────────┐
        │   25-bit grid payload  │ checksum │
        └────────────────────────┴──────────┘
         packed >> 5               packed & 31

Encoding is three steps:

  1. XOR the grid with a constant (0x13A7C5B). Without this, boards that differ by one cell produce codes that differ in one place, and a nearly-empty board produces a code that is nearly all A. The XOR scatters them.
  2. Append 5 check bits mixed from the whole payload.
  3. Slice into six 5-bit symbols and index the alphabet.

Decoding reverses it, then rejects the empty grid — a board with no lights on is already solved, so it is never a valid stage.

The alphabet

ABCDEFGHJKLMNPQRSTUVWXYZ23456789

32 symbols with I, O, 0 and 1 removed. Those are the two pairs people actually confuse when copying a code by hand, and dropping them costs nothing — the remaining 32 is exactly the power of two needed for 5 bits per symbol.

This is the cheapest reliability win in the whole design. It eliminates a class of transcription error before the checksum ever has to catch it.

The checksum

unsigned checksum(unsigned payload)
{
    unsigned mixed = payload ^ (payload >> 7) ^ (payload >> 13) ^ 0x1Du;
    mixed *= 0x15u;
    mixed ^= mixed >> 5;
    return mixed & 31u;
}

Two right-shifted XORs fold high bits down into the low five, so a change anywhere in the 25-bit payload reaches the check bits. The multiply and final shift-XOR diffuse them further. Four operations, no lookup table.

Measured behaviour

Every figure below comes from verify/verify.py, which mirrors the C++ exactly — including the 32-bit unsigned wraparound on the multiply, which is what the hardware does. Seeded, so it reproduces.

ROUND TRIP            1,000,000 random grids        0 failures

SINGLE-CHARACTER TYPO 930,000 mutations
  rejected            903,161                       97.11%
  silently wrong       26,839                        2.89%

ADJACENT TRANSPOSITION 96,916 swaps
  rejected             93,404                       96.38%

RANDOM 6-CHAR STRINGS  50,000 tested
  accepted              1,516                        3.03%
                                    theoretical floor 3.125%

CHECKSUM DISTRIBUTION  200,000 payloads
  values seen          32/32
  min 6,058   max 6,437   ideal 6,250

Two things worth drawing out.

Random strings are accepted 3.03% of the time, against a theoretical floor of 3.125%. With 5 check bits you cannot do better than 1-in-32. This checksum is at the limit — the remaining gap is sampling noise, not headroom.

Single-character typos are caught 97.11% of the time. The 2.89% that slip through load a different valid board rather than an error, which is the failure mode to be honest about: the player sees a puzzle, just not the one they were given. Eliminating that entirely would need more check bits, which would mean a seventh character.

That trade — one extra character for the last 3% — is the interesting design decision in the whole system, and it went the other way deliberately. Six characters is meaningfully easier to say out loud than seven.

Usage

Header-only, freestanding, C++17. No allocation, no dependencies.

#include "closing_time_codec.hpp"

char code[7] = {};
closing_time::encode(0x000ABCDE, code);      // -> "VBSEFD"

auto result = closing_time::decode(code);
if(result.valid)
{
    unsigned grid = result.grid;             // 25-bit board
}

encode writes exactly 6 characters and adds no terminator. decode returns valid = false for unknown symbols, checksum failures, and the empty grid.

Both are constexpr, so codes can be baked at compile time.

Verifying

python3 verify/verify.py

Notes

The game applies two further rules on top of the codec, which live in the game rather than here because they are about Lights Out specifically:

  • the board must be solvable (checked with a brute-force chase solver)
  • it must need at least 2 moves, so a one-press board is not shareable

The codec is deliberately unaware of both. It transports 25 bits; whether those bits are an interesting puzzle is not its problem.

Licence

MIT — see LICENSE.

The codec algorithm and constants are from Closing Time by Sarah C.

About

Packing a 5x5 Lights Out board into six characters a person can read aloud

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages