BPSE Puzzle Format
BPSE (Binary Puzzle State Encoding) is the compact serialisation format Lazy Sudoku uses to encode a complete puzzle state — including the grid, candidates, given/solved status, user annotations, and game metadata — into a URL-safe string.
When you copy a puzzle using the BPSE format (the default in Copy Puzzle), the resulting string looks like this:
eNqbzDiFgZHhwJQjMUbMHYoNjlxSDlq6XubLeI9sUfO...
This page describes the format in detail, so you can write your own parser if you want to interoperate with Lazy Sudoku puzzle strings.
Encoding Pipeline
A BPSE string is produced in three steps:
Puzzle State → MessagePack → zlib deflate → base64url
To decode, reverse the pipeline:
base64url → zlib inflate → MessagePack → Puzzle State
base64url uses the URL-safe alphabet (RFC 4648 §5): A-Z, a-z, 0-9, -, _, with no padding characters.
Top-Level Structure
The MessagePack payload is an array of 3 elements:
| Index | Name | Type | Description |
|---|---|---|---|
| 0 | version | uint8 | Format version (currently 1) |
| 1 | header | array(4) | Game metadata |
| 2 | body | array(4 or 5) | Puzzle state data |
Parsers should check the version first and reject unknown versions.
Header
The header is a fixed array of 4 elements:
| Index | Name | Type | Values |
|---|---|---|---|
| 0 | game_type | uint8 | 0 = Classic 9×9, 1 = Jigsaw 9×9 |
| 1 | game_profile | uint8 | 0 = Relaxed, 1 = Standard, 2 = Deep |
| 2 | pencil_flags | uint8 | Bitfield (see below) |
| 3 | puzzle_id | string or nil | Puzzle identifier (e.g. "jade-drey") |
Pencil Flags
| Bit | Mask | Meaning |
|---|---|---|
| 0 | 0x01 |
Paper mode enabled |
| 1 | 0x02 |
Housekeeping (auto-eliminate from peers on placement) |
| 2 | 0x04 |
Full paper mode (immersive stylus view) |
Body (Classic 9×9)
The body is an array of 4 elements (or 5 in paper mode):
| Index | Name | Type | Size | Description |
|---|---|---|---|---|
| 0 | candidates | bin | 92 bytes | Engine candidate bitmap |
| 1 | givens | bin | 11 bytes | Given-cell mask |
| 2 | solved | bin | 11 bytes | Solved-cell mask |
| 3 | notes | map | variable | Sparse note overrides |
| 4 | user_candidates | bin | 92 bytes | User candidate bitmap (paper mode, only when divergent) |
Candidates — bin(92)
A packed bitfield of 729 bits (81 cells × 9 digits), stored as 92 bytes in little-endian bit order.
Bit addressing: bit index cell × 9 + (digit - 1), where cell is 0–80 (row-major: cell = row × 9 + col) and digit is 1–9.
- For empty cells, set bits represent the engine's candidate digits.
- For given/solved cells, exactly one bit is set — this encodes the placed value.
- Padding: the 7 trailing bits (729–735) are zero.
To extract byte and bit position for a given cell and digit:
bit_index = cell * 9 + (digit - 1)
byte_index = bit_index / 8
bit_offset = bit_index % 8
is_set = (bytes[byte_index] >> bit_offset) & 1
Givens — bin(11)
An 81-bit mask packed into 11 bytes (little-endian bit order). Bit i is set if cell i is an immutable given.
Solved — bin(11)
Same layout as givens. Bit i is set if cell i has been solved by the user (not a given).
Notes — map
A MessagePack map encoding only non-default note states. Keys are cell indices (uint8, 0–80). Values are inner maps where keys are digits (uint8, 1–9) and values are note states (uint8):
| Value | State |
|---|---|
| 2 | Excluded (crossed out by user or technique) |
| 3 | Highlighted (user-highlighted) |
Cells with no overrides are omitted. An empty puzzle has an empty map ({}).
Example: cell 5 has digit 3 excluded and digit 7 highlighted:
{ 5: { 3: 2, 7: 3 } }
User Candidates — bin(92) (paper mode, divergent only)
Present only in paper mode (pencil_flags bit 0 set) and only when the user's candidates differ from the engine candidates. Same layout as the engine candidates bitmap, but represents what the user has actually pencilled in.
In paper mode, the engine bitmap and user notes can diverge — the engine tracks all valid candidates, while the user manages their own notes by hand. When the two are identical (e.g. the user has filled in all valid candidates), this field is omitted to save space. When absent, a parser should derive note states from the engine candidates bitmap.
Body (Jigsaw 9×9, future)
For game_type 1, the body will include a 5th element (or 6th in paper mode):
| Index | Name | Type | Size | Description |
|---|---|---|---|---|
| 4/5 | box_assignments | bin | 81 bytes | One byte per cell, box index 0–8 |
Size
A typical puzzle with 25 givens and auto-filled candidates produces a BPSE string of ~170–180 characters. With puzzle ID and note overrides, expect ~200–250 characters. Paper mode adds the user bitmap but zlib compresses the redundancy well.
For comparison, the SudokuWiki S9B format is 165 characters but carries less information (no game profile, pencil mode, puzzle ID, or excluded/highlighted annotations).
Detecting BPSE Strings
A BPSE string consists entirely of base64url characters (A-Za-z0-9-_), is typically 100–300 characters long, and does not start with S9B (which identifies the SudokuWiki format) or look like an 81-character digit string. Lazy Sudoku's parser tries BPSE decoding for any input that is all base64url characters and is not a valid grid string.
Reference Implementation
The canonical implementation is in the Lazy Sudoku source code:
- C++ encoder/decoder:
core_engine/include/sudoku/puzzle_serial.hpp - CLI:
sudoku convert --to bpse <puzzle>to encode, any subcommand accepts BPSE as input