# Board & puzzle formats, written down

> Every format an Eternity II board or puzzle travels in on this site and in the community: the board_edges letter string and the hints clue list, e2pieces.txt, the puzzle CSV, the site's Puzzle JSON, and the viewer URL, with the exact byte-level rules (how the grey border is encoded in each) and, most of all, what each format can and cannot recover.

- Canonical page (with interactive figures/demos): https://eternity2.dev/research/build/formats/
- Updated: 2026-07-16
- Source: The canonical IO layer that defines these formats (e2-io: the one board format, derived everywhere) — https://github.com/raphael-anjou/eternity2/tree/main/research/experiments/common/crates/e2-io

---
A board is a simple thing, 256 tiles, each with four coloured edges, each
placed at a cell and a rotation, but the community has written it down a dozen
ways over nineteen years, and the ways never quite line up. A letter string, a
digit string, a URL, a CSV, a JSON. They say the same thing in different
alphabets, and the moment you move a board between two tools you hit the seam.

This page is the seam, written down: every format a board or puzzle travels in
here, with the byte-level rules, including the one detail that bites everyone,
**how each format encodes the grey border**, and, for each, a plain note on
what it can and cannot recover. If you only take one thing away: paste any board
into the [format converter](/convert) and read the others straight back.

> **One input, one output**
>
> The IO contract is symmetric and small. **In:** one puzzle instance as JSON (the site `Puzzle` schema, below); every algorithm is invoked with `--puzzle <file>.json`. **Out:** one board document as JSON (the `BoardDoc`, below), carrying a ready-to-open [eternity2.dev](/viewer) URL as a field. That is the whole thing: puzzle-JSON in, board-JSON out, both single self-describing files. Everything else on this page is either an *input* the tools also accept (CSV, a bare `board_edges` blob, a bucas link) or a *derived* format the [converter](/convert) can hand you. There is no `.url`-here, `.json`-there, two-files-sometimes split.

## The vocabulary

A **colour** is a small integer. `0` is the grey outer border; interior colours
run `1..=22` for the official puzzle (22 is exactly how many motifs exist). An
**edge quad** is a cell's four colours in **URDL** order (up, right, down, left),
reading the tile *as placed and rotated*, not as a catalogue tile. A cell is
either filled (a real quad) or empty; an empty cell is written as if it were all
border, `[0, 0, 0, 0]`.

One fact follows from "as placed", and it is the source of most format
confusion: a board carries **rotated** edges, not a rotation number. But this is
not a loss. Eternity II pieces are **all distinct up to rotation** (the official
set is, and any faithful generator keeps it that way), so a cell's edge quad
identifies exactly one piece, and the rotation is the turn that maps that
piece's catalogue quad onto the placed one. Matching the quad against the piece
set recovers **both** the identity and the rotation.

So the edges are the whole board: they render it, score it, and name every
piece and its orientation. The only case where they fall short is a contrived
set with two pieces sharing an edge pattern up to rotation, which real Eternity
II never has, and there a piece-number channel disambiguates. That is the one
job `board_pieces` exists for.

## board_edges: the letter string

The lingua franca. Four lowercase letters per cell, row by row, URDL, with
`letter = 'a' + colour`. So the border is **`a`**, interior colour 1 is `b`, up
to colour 22 = `w`. An empty cell is **`aaaa`**, indistinguishable from a real
all-border tile, which never happens on a legal board, so the ambiguity is
harmless. The official 16×16 board is `256 × 4 = 1024` letters.

```text
adca beaa …            cell 0 = [0,3,2,0], cell 1 = [1,4,0,0], …
```

- **Border:** the letter `a` (colour 0).
- **Recovers:** everything about the board, the full rendering, the exact score,
  and, because pieces are distinct up to rotation, each piece's identity and its
  rotation by matching against the set.
- **Cannot recover:** which cells were *given as clues* rather than solved, that
  is a property of the puzzle, not the board, and it travels in `hints`.

A rare wrinkle: some tools permute which glyph each letter draws
(`motifs_order`). The orders `marie` and `jblackwood` are identical to each
other; `jef` is a different permutation. The converter and viewer translate
these back to the default alphabet on the way in, so you never have to think
about it, but if you see a board whose colours look scrambled, an untranslated
`motifs_order` is why.

## hints: the clue cells

A board and the clues it was built from belong in one place, addressed the same
way. `hints` lists the pinned clue cells, each as `pos.rot`, the row-major cell
index and clockwise quarter-turn, joined by `-`. Five official clues are about
thirty characters.

```text
hints=34.1-45.2-135.0-210.3-221.0
```

The piece at each hinted cell is already known from `board_edges`, so a clue
needs only its position and orientation, never a piece id. This is the same
`(cell, rotation)` shape every durable format uses for a pinned tile: the CSV's
`x,y,rotation`, the site JSON's `{pos, rot}`.

- **Recovers:** which cells are clues, so the viewer can mark them and a solver
  can start from them. Absent means "no clues", a plain solved or partial board.
- **Pairs with:** `board_edges`. Together they carry a full *puzzle-with-clues*
  in one link, which a bare board never could.

## board_pieces: the interop channel

Some community tools, notably e2.bucas.name links, carry an explicit piece
channel: three decimal digits per cell, the **1-based** piece number, `000` for
an empty cell (the official board is `256 × 3 = 768` digits). We **read** it when
it is present, but never need to emit it, because `board_edges` already names
every piece on a distinct-piece set.

```text
001 005 007 …          piece 1 at cell 0, piece 5 at cell 1, …
```

- **Border / empty:** `000`.
- **Recovers:** piece identity directly (and rotation, matched against the set),
  the same thing the edges recover on their own for a real board.
- **On its own:** useless without the edges; it is an overlay, not a board. Its
  one irreplaceable use is a contrived set with pieces that repeat up to
  rotation, where the edges alone are ambiguous.

## e2pieces.txt: the pieces as placed

The oldest interchange format, written down by the community in 2007 (see the
[toolbox census](/research/build/tooling)). One line per **filled** cell, in
reading order, four whitespace-separated edge integers in URDL order.

```text
0 3 2 0
1 4 0 0
…
```

- **Border:** the integer `0`.
- **Recovers:** rendering and score, same as `board_edges`; it is the same data
  in decimal. It lists the pieces **as placed and rotated**, so it is *not* a
  canonical rotation-0 catalogue, and a board alone cannot turn it back into one.

## Puzzle CSV: what the standalone engines read

The format McGavin's C and Blackwood's C# consume, and the shape the benchmark's
`variant_NN.csv` files are written in. A size header, then one row per piece:
`top,right,bottom,left` followed optionally by `x,y,rotation` for a pinned hint.

```text
16
1111111111111111,0000000000000001,0000000000000010,1111111111111111,0,0,0
…
```

Each colour is a **16-bit zero-padded binary word**. And here is the detail that
trips everyone, and the reason this page exists:

> **The border is all ones in the CSV**
>
> In the puzzle CSV, the grey border is **`1111111111111111`**, all ones, the 16-bit word `65535`, *not* all zeros. Interior colours are their small integer in binary, so colour 1 is `0000000000000001`. A reader maps the `65535` word back to interior id `0`. This is the opposite of every other format on this page, where the border is the *low* value (`a`, `0`, `000`). It is a legacy sentinel choice, kept for byte-compatibility with the engines that read it.

- **Border:** the all-ones word `1111111111111111` (65535).
- **Recovers:** the full puzzle, pieces and hints. This is a *puzzle* format
  (the tile set), not just a placed board.

## Site JSON (`Puzzle`): the canonical input

The one format every algorithm is fed: `--puzzle variant_NN.json`. The site's
engine, the DFS/repair studies and the single-core benchmark all read the *same*
schema (byte-compatible with the web's `Puzzle` type), so every experiment runs
the same instances. It carries the piece set at rotation 0 and the pinned hints
explicitly, which is what makes it an input a solver can start from, rather than
a board it has already filled.

```json
{
  "name": "e2_official_variant00_pin3corners",
  "width": 16, "height": 16, "numColors": 22,
  "pieces": [[0,1,2,0], [0,0,3,1], …],
  "hints": [{ "pos": 34, "piece": 118, "rot": 2 }, …]
}
```

- **Border:** colour `0`, as an integer in the `pieces` quads.
- **Recovers:** everything about the *instance*: canonical rotation-0 tiles,
  colour count, and every pinned clue with its exact rotation. This is the one
  format that carries rotation and catalogue order by construction, because it
  describes the puzzle, not a placement on it.

## The canonical board JSON (`BoardDoc`): the one output

What every solver on this site now writes: one self-describing `.json` per
board. It folds the board, its score, its content hash, both letter strings, and
the viewer URL into a single document, so a downstream tool needs nothing else.

```json
{
  "name": "…",
  "size": 16,
  "score": 466,
  "breaks": 14,
  "board": [3, 16, 24, …],
  "board_hash": 2059303548241384230,
  "board_edges": "abda…",
  "board_pieces": "001005…",
  "url": "https://eternity2.dev/viewer?puzzle=…&puzzle_size=16&board_edges=…"
}
```

`board` is the row-major **`piece*4 + rot`** placement vector (`-1` for an empty
cell), the same encoding the site's `Puzzle` round-trips through. `breaks` is
`max_score − score` (the unmatched interior edges; on a full board, the break
count). `board_hash` is an FNV-1a hash of the placement grid, the bit-identical
gate for "did these two runs produce the same board".

- **Border:** as in `board_edges` (`a`) and `board_pieces` (`000`); the `board`
  vector uses `-1` for empty.
- **Recovers:** everything a board can: rendering, score, identity and rotation
  (via `board_pieces` and `board`), plus provenance (`name`, `board_hash`) and a
  one-click view (`url`).

## The viewer URLs

Two URLs, same board, two hosts.

**eternity2.dev, the canonical link.** The format every algorithm here emits.
The board rides entirely in `board_edges`; any pinned clues ride in `hints`.
Square boards, so a single `puzzle_size` (the viewer also reads bucas's
`board_w`/`board_h`), and every value stays in the URL-safe range so the link
needs no percent-encoding:

```text
https://eternity2.dev/viewer?puzzle=NAME&puzzle_size=16&board_edges=…&hints=135.0-210.1
```

`hints` is omitted for a board with no clues. Because the edges carry piece
identity, there is no piece channel here: the board and its clues fit in these
two fields.

**e2.bucas.name, the community viewer.** Jef Bucas's original, the viewer whose
URL format *became* this whole vocabulary (the [toolbox](/research/build/tooling)
tells that story). Fully supported as an input, and available as a derived
output using its `board_w`/`board_h` pair and a `#` fragment. We emit it
edges-only (bucas recovers the pieces from the edges for a distinct set), though
bucas links from other tools often also carry a `board_pieces` channel, which we
read on input:

```text
https://e2.bucas.name/#puzzle=NAME&board_w=16&board_h=16&board_edges=…
```

The [viewer](/viewer) reads either transparently; the [converter](/convert)
hands you both. The eternity2.dev form is canonical here only because it opens
*this* site's viewer, with its scoring, clue overlay and piece-set verification;
the bucas form remains the community's shared original, credited accordingly.

## Moving between them

<div className="not-prose">

| Format | Border | Carries rotation? | Carries identity? | It is a… |
| --- | --- | --- | --- | --- |
| `board_edges` | `a` | yes (by match) | yes (by match) | placed board |
| `hints` | n/a | yes (`pos.rot`) | via edges | clue set |
| `board_pieces` | `000` | with edges | yes | overlay |
| `e2pieces.txt` | `0` | yes (by match) | yes (by match) | placed board |
| Puzzle CSV | `1111…1` (all ones) | hints only | yes (tiles) | puzzle |
| Site JSON | `0` | yes | yes | puzzle |
| `BoardDoc` JSON | `a` / `000` / `-1` | yes | yes | placed board |
| eternity2.dev URL | `a` | yes (by match) | yes (by match) | link to a board |
| bucas URL | `a` | with pieces | with pieces | link to a board |

</div>

Every conversion in that table is one paste away in the
[format converter](/convert): drop in a URL, a bare `board_edges` string, or a
params blob, and read back the eternity2.dev URL, the canonical JSON, the CSV,
`board_pieces`, `e2pieces.txt`, and the bucas URL, with a live preview and the
score, so you can see the file is right before you trust it.

## Related

- [The community's toolbox, 2007–2026](https://eternity2.dev/research/build/tooling) — Nineteen years of community software for Eternity II (manual-placement GUIs, editors, public solvers, generators and viewers), plus the quieter layer that made them interoperate: e2pieces.txt, CRC-16 checksums, and the board-in-a-URL format that became the lingua franca. A reference census, with every tool sourced to the message that announced it.
- [The community's benchmarks](https://eternity2.dev/research/build/benchmarks) — How a community forbidden from sharing the pieces built a shared test culture anyway: derived-count verification protocols, the Txibilis and beginner suites, node-count duels, full enumerations, and the one benchmark that is still standing open today.
- [The dataset](https://eternity2.dev/research/build/dataset) — A public, CC0 dataset for Eternity II in two parts: fourteen benchmark instances to solve, and a corpus of 7,658 distinct strong boards to learn from. Every score is recomputed from the board itself, and the corpus is checked to be genuinely diverse rather than a thousand copies of one board.
