All extensions

@open-rgs/grid

Shape is a height per column, not width by height.

Ragged is not a special case

A shape is a list of column heights. Rectangular boards are the case where every entry happens to match, so a 4-5-5-5-5-4 reel set needs no separate code path.

The fifth cell exists in the tall reels and is simply absent from the short ones.
import { rect, makeGrid } from "@open-rgs/grid";

const SHAPE  = rect(5, 3);
const RAGGED = [4, 5, 5, 5, 5, 4];

const board = makeGrid(RAGGED, () => "LOW");

Off the grid reports, never throws

On a ragged board an out-of-range row is normal, not exceptional - a payline crossing a short reel simply has no cell there. So reads report their absence instead of raising.

Cells are stored flat, column-major. One array per board, not one per column.
import { at, indexOf, countOf } from "@open-rgs/grid";

at(board, 1, 4);
at(board, 0, 4);
indexOf(RAGGED, 0, 4);

countOf(board, "LOW");

Neighbours are asked for

Cluster evaluation walks neighbours, and on a ragged board a cell can have a left neighbour and no right one. Computing that from width and height gets it wrong; asking the shape does not.

Only the neighbours that exist on this shape come back.
import { neighbours, positionsWhere } from "@open-rgs/grid";

neighbours(RAGGED, { col: 1, row: 4 });

positionsWhere(board, (s) => s === "SC");