The engine’s big-symbol mechanism doesn’t hard-code w === h. Any positive integer size works. what changes per shape is the visual intent and the grid budget it consumes. This recipe walks through the four most common shapes one at a time, with the placement logic shown in each block. Each demo always plants its shape so every spin shows the mechanic.
One thing first. Every shape uses the same registration API:
symbolData({ id: { weight: 0, size: { w, h } } }). The differences below are about when to reach for each shape, not how to wire it. If you only read one section, scroll to How the wire format stays unchanged at the bottom. that’s the bit that makes the rest make sense.
Square. 2×2
Loading recipe…
The most common big-symbol shape. Equal width and height means it fits on any 5+ reel grid with 3+ rows without distorting the grid’s aspect ratio.
const SQUARE = { id: 'square', size: { w: 2, h: 2 } };
builder
.symbols((r) => r.register('square', SpriteSymbol, { textures: { square: tex } }))
.symbolData({ square: { weight: 0, zIndex: 5, size: { w: 2, h: 2 } } });
Anchor logic. the recipe always plants a 2×2 to make the demo deterministic:
const col = Math.floor(Math.random() * (REELS - 2 + 1)); // last legal anchor col
const row = Math.floor(Math.random() * (ROWS - 2 + 1)); // last legal anchor row
grid[col][row] = 'square';
// engine paints OCCUPIED at (col+1,row), (col,row+1), (col+1,row+1)
When to use: sprites authored at 2× resolution, where the symbol takes up four cells of the grid. Default first-thought big-symbol shape.
When not: narrow grids (e.g. 3×3, where a 2×2 covers most of the board). Symbols that fit better in a single tall column (use 1×3 below).
Tall bar. 1×3
Loading recipe…
One column, three rows. The block stays inside a single reel. it’s effectively a “stacked” symbol. Reads as a totem, gold-bar tower, or a vertical wild column that fills its reel without spilling into neighbours.
const TALL = { id: 'tall', size: { w: 1, h: 3 } };
builder.symbolData({ tall: { weight: 0, zIndex: 5, size: { w: 1, h: 3 } } });
Anchor logic. same placement formula, different size budget:
const col = Math.floor(Math.random() * (REELS - 1 + 1)); // any column (w=1)
const row = Math.floor(Math.random() * (ROWS - 3 + 1)); // last legal anchor row
grid[col][row] = 'tall';
// engine paints OCCUPIED at (col, row+1), (col, row+2)
When to use: vertical wilds (Wild Toro–style), gold-bar / coin towers, “stacked symbol” effects without the per-cell duplicate logic of true stacked reels.
When not: anything that needs to span columns. A 1×N stays inside a single reel.
Mask note. Single-column blocks don’t cross reel gaps, so
RectMaskStrategy(the default) clips them correctly even whensymbolGap.x > 0. The auto-pick ofSharedRectMaskStrategyonly fires for blocks wherew > 1. The recipe still usesSharedRectMaskStrategyhere for visual consistency with the other blocks.
Giant. 3×3
Loading recipe…
A board-dominating feature anchor. On a 6×5 grid a 3×3 covers nine cells. half the visible area on the rows it lands across. Use sparingly: this is “free spins triggered” energy, not “weighted symbol” energy.
const GIANT = { id: 'giant', size: { w: 3, h: 3 } };
builder.symbolData({ giant: { weight: 0, zIndex: 5, size: { w: 3, h: 3 } } });
Anchor logic. the legal anchor range is much smaller because the block is so large:
const col = Math.floor(Math.random() * (REELS - 3 + 1)); // 4 legal columns on a 6-reel grid
const row = Math.floor(Math.random() * (ROWS - 3 + 1)); // 3 legal rows on a 5-row grid
grid[col][row] = 'giant';
// engine paints OCCUPIED at the other 8 cells of the 3×3 block
When to use: rare feature reveals. bonus rounds, jackpot panels, character splash art. Also good when the symbol is the “frame” for an internal mini-grid (multiplier coins, prize cells).
When not: small grids (5×3. a 3×3 leaves only one valid anchor position). Random fill (weight: 0 is mandatory; the engine throws on non-zero anyway). Frequent occurrences. at this size, players need it to feel like an event.
Wide banner. 2×4
Loading recipe…
Two columns, four rows. A block that’s taller than wide. a banner pose rather than a square. Useful when you want to span adjacent reels (so the symbol clearly belongs to multiple reels’ lines / ways) but still want a bold vertical presence.
const WIDE = { id: 'wide', size: { w: 2, h: 4 } };
builder.symbolData({ wide: { weight: 0, zIndex: 5, size: { w: 2, h: 4 } } });
Anchor logic. same shape of formula, but h: 4 requires rows >= 4:
const col = Math.floor(Math.random() * (REELS - 2 + 1)); // 5 legal columns on a 6-reel grid
const row = Math.floor(Math.random() * (ROWS - 4 + 1)); // 2 legal rows on a 5-row grid
grid[col][row] = 'wide';
// engine paints OCCUPIED at the other 7 cells of the 2×4 block
When to use: “feature triggered” splash panels, scroll-effect tall character art that should be visually anchored to a pair of reels (so paylines / ways through it count both columns).
When not: grids shorter than h. the engine will throw at setResult() with "block exceeds reel height" because the anchor at row 0 already needs row 0..3 visible.
How the wire format stays unchanged
The server response stays string[][] for every shape above. The server sets the symbol id at the anchor cell (top-left) only. The engine:
- Cross-reel coordinator runs in
SpinControllerahead of per-reelFrameBuilder. It reads the result grid, finds big-symbol anchors, validates that each block fits, and paints the internalOCCUPIED_SENTINELacross the rest of the block. - At land, the anchor symbol is sized to
(w * cellW, h * cellH). Non-anchor cells get an invisibleOccupiedStub. the anchor’s view spans all of them visually. - Public API never sees the sentinel.
Reel.getVisibleSymbols()andReelSet.getVisibleGrid()resolve OCCUPIED. both same-reel and cross-reel. to the anchor’s id.
// 6×5 grid with a 2×4 wide banner anchored at (col=2, row=1):
setResult([
['low1', 'low2', 'low1', 'low2', 'low1'],
['low1', 'low2', 'low1', 'low2', 'low1'],
['low1', 'wide', '?', '?', 'low2'], // wide anchor + 'wide' OCCUPIED below
['low1', '?', '?', '?', 'low2'],
['low1', '?', '?', '?', 'low2'],
['low1', 'low2', 'low1', 'low2', 'low1'],
]);
The ? cells can be any string; the engine overwrites them. Keep them readable for debug logs.
Validation. fail-fast at setResult()
If the server places a block that doesn’t fit, setResult throws synchronously:
big symbol 'giant' (3x3) at (col=4, row=2) exceeds reel count 6.
big symbol 'tall' (1x3) at (col=0, row=3) exceeds reel 0 height 5.
Catch in your server-integration layer if you want graceful behaviour (re-roll, fallback to a 1×1, log telemetry). The engine’s contract is “shape your data correctly, or fail loud.” Silent truncation would let bad placements slip into production.
The build-time check refuses big symbols with non-zero weight. Random fill cannot place blocks in v1, so a non-zero weight would silently never be picked. the builder throws with big symbol 'X' (NxM) must have weight 0. ....
Footprint and block bounds
Two related APIs work for any cell. anchor or non-anchor:
reelSet.getSymbolFootprint(col, row)→{ anchor: { col, row }, size: { w, h } }. Use for evaluation, event payloads, win-line logic.reelSet.getBlockBounds(col, row)→{ x, y, width, height }covering the whole block in ReelSet-local pixels. Use for overlays, win frames, cluster outlines.
const rect = reelSet.getBlockBounds(col, row);
// 1×1 cell? rect equals getCellBounds(col, row).
// Big-symbol cell? rect spans the entire block, regardless of which cell you passed.
gfx.rect(rect.x, rect.y, rect.width, rect.height).stroke({ color: 0xff6b35, width: 4 });
reelSet.addChild(gfx);
Authoring resolution
Pre-render each big symbol’s texture at the size it’ll actually display. A 3×3 giant on a 70 px-cell grid is 210 × 210. author at that resolution or larger. Sprites scale up poorly. If you must support multiple cell sizes (responsive grids, MultiWays. but big symbols are rejected on MultiWays in v1), ship per-size variants or use a Spine skeleton.
Constraints (all shapes)
- Big symbols + MultiWays. rejected at build. Block sizes don’t compose with per-spin shape changes (“what’s a 2×2 on a 2-row reel?” is a game-design question we don’t answer in v1).
- Random fill doesn’t generate big symbols in v1. Only target frames place them. v2 may add a
RandomFillBigSymbolMiddleware. - During spin, every cell is 1×1; the block layout commits at landing.
- Pin a non-anchor cell throws. pin the anchor instead, which covers the block visually.
Related
- Guide: Per-reel geometry, MultiWays & big symbols. full mechanics primer
- Big symbols (basics). single 2×2 example with full architecture notes
- getBlockBounds (overlays). drawing block-aware win frames
- MultiWays. mutually exclusive with big symbols