Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Grids and Coords

Almost every Nikoli + Sudoku-variant puzzle lives on a square grid; the only outlier worth treating specially is hex. So the standard layout is a square cell grid with three companion sub-grids over the same coordinate system:

LayerDimensionsUsed by
CellsN × MSudoku, Akari, Nurikabe, Heyawake, Hitori, Masyu
Edges(N+1) × M horizontals + N × (M+1) verticalsSlitherlink, fence rules
Corners(N+1) × (M+1)rare; Masyu pearls have corner-flavored constraints

A single puzzle can host constraints across any mixture of sub-grids. Most genres use one; Slitherlink uses cells (for clues) and both edge grids (for the loop). The four sub-grids and a universal coordinate type are library-defined:

/// Reference enum for *talking about* layers — used by
/// `Region::AllOf(Layer)`, `Coord::layer()`, `Substrate::grid(layer)`.
pub enum Layer {
    /// `N × M` cell grid. Default for almost every puzzle.
    Cells,
    /// `(N+1) × M` horizontal edges (between vertically-adjacent cells).
    EdgesH,
    /// `N × (M+1)` vertical edges (between horizontally-adjacent cells).
    EdgesV,
    /// `(N+1) × (M+1)` corners.
    Corners,
}

/// A position in any sub-grid. The library's universal coordinate
/// type — every Nikoli/Sudoku-variant genre shares it. Hex puzzles
/// and other non-square layouts live behind a separate `HexPuzzle`
/// trait family with their own coord type.
pub enum Coord {
    Cell   { r: usize, c: usize },
    EdgeH  { r: usize, c: usize },
    EdgeV  { r: usize, c: usize },
    Corner { r: usize, c: usize },
}

Layer is the discriminator that constraints and regions use to say which sub-grid they’re talking about; Coord is the value-level position they resolve to. A Slitherlink puzzle can carry cell clues alongside edge state without ambiguity because every constraint ultimately bottoms out at Coord values.

Coordinates. Internal: (row, col) zero-indexed from the top- left, so (0, 0) is the upper-left cell — matches array indexing and how every renderer naturally walks.

External (URLs, shared puzzle strings, REPL output) is layer-prefixed and chess-up-numbered: <layer>:<col><row>, where the column is a bijective base-26 letter sequence (a, b, …, z, aa, ab, …, az, ba, …) and the row is a 1-indexed integer counted from the bottom up (so cell:a1 is bottom-left, matching chess and Sudoku convention). The bijective base-26 scheme handles arbitrarily wide boards — a 30-column Akari grid uses a..z then aa..ad; the chess 8-column ceiling does not apply.

Each layer has its own grid dimensions and is addressed within that layer’s own coordinate space:

PrefixLayerGridExample
cell:CellsN × Mcell:a1 = bottom-left cell
edgeh:EdgesH(N+1) × Medgeh:a1 = bottommost horizontal edge of column a
edgev:EdgesVN × (M+1)edgev:a1 = leftmost vertical edge of row 1
corner:Corners(N+1) × (M+1)corner:a1 = bottom-left corner

The cell: prefix may be omitted (cells are the default layer), so a1 and cell:a1 parse identically. The other prefixes are required.

A human-friendlier alternate form references neighboring cells — edgeh(a1,a2) for the horizontal edge between cells a1 and a2, edgev(a1,b1) for the vertical edge between a1 and b1, corner(a1,b1,a2,b2) for the corner shared by those four cells. Both forms parse to the same Coord; the canonical <layer>:<col><row> is what the wire format uses, and the alternate form is sugar for hand-typed input.

Grid storage

Layer contents — substrate Material markings, per-coord candidate bitmasks, render glyph caches — all share one storage type: a flat row-major Grid<T>, in core::grid.

pub struct Grid<T> {
    rows: usize,
    cols: usize,
    data: Vec<T>,
}

impl<T: Default + Copy> Grid<T> {
    pub fn new(rows: usize, cols: usize) -> Self;        // filled with default
    pub fn get(&self, r: usize, c: usize) -> T;
    pub fn set(&mut self, r: usize, c: usize, v: T);
    pub fn iter(&self) -> impl Iterator<Item = ((usize, usize), T)>;
    pub fn data(&self) -> &[T];
}

Flat storage gives O(1) index, no hash overhead. The Default + Copy bound covers every layer payload the catalog reaches for — Material and Mark are both small Copy enums that fit it trivially.