All extensions

@open-rgs/symbols

What a symbol turns into after the board is drawn.

A generator decides what lands. These decide what it means: the tile that flips, the lows that upgrade, the wild that walks, the wild that multiplies, the tall symbol that counts twice. They run in the same place, after the draw and before the evaluator, and each has one rule that decides whether the game is priced correctly.

Mystery tiles reveal together

Reveal before evaluating: a mystery tile is not a paying symbol, so an evaluator that sees one reads a board the player never had. All tiles turning over to the same symbol is the convention, and it is a variance decision rather than a cosmetic one, since it correlates the cells perfectly.

21 ?73?7 ?71
Hidden tiles turn over together, to one symbol.
import { revealMystery } from "@open-rgs/symbols";

const { grid, revealed, cells } = revealMystery(board, "MYSTERY", SYMBOLS, host.rng_next);

// one draw per tile instead
revealMystery(board, "MYSTERY", SYMBOLS, host.rng_next, { shared: false });

Transforms happen in order

Order is the whole behaviour. LOW -> HIGH followed by HIGH -> WILD turns the lows into wilds, which is rarely what the paytable was priced for. upgradeLadder walks the ladder from the top down, so one call moves every symbol exactly one rung and the top rung stays put rather than wrapping.

LL LH L
Every low on the board becomes a premium, in one call.
import { transform, transformAll, upgradeLadder } from "@open-rgs/symbols";

transform(grid, "LOW", "PREM");                       // and the cells it touched
transformAll(grid, [["L1", "L2"], ["L2", "L3"]]);     // in this order
upgradeLadder(grid, ["L1", "L2", "L3", "L4"]);        // each moves one rung

Walking wilds are re-applied, then stepped

Same rule as a sticky cell: written onto every fresh board, or it does not walk. Stepping drops the ones that left the board, so "the feature ends when the last wild walks off" is the length of the list rather than a counter kept in step with it.

import { applyWalkers, stepWalkers } from "@open-rgs/symbols";

let walkers = [{ pos: { col: 4, row: 1 }, symbol: "WILD", step: -1 }];

const board = applyWalkers(drawBoard(next), walkers);   // before evaluating
walkers = stepWalkers(board, walkers);                  // after
const featureOver = walkers.length === 0;

A walker moving onto a shorter reel lands on that reel's lowest row rather than falling off, which is what a ragged board needs and what a rectangle-shaped implementation gets wrong.

Multipliers on one win multiply

Two 2x wilds in the same line pay 4x, not 3x. That is the convention, and the difference shows up at the top of the distribution, which is the part that gets checked last.

import { noFactors, withFactor, winFactor } from "@open-rgs/symbols";

let factors = noFactors();
factors = withFactor(factors, { col: 0, row: 1 }, 2);
factors = withFactor(factors, { col: 2, row: 1 }, 3);

win.multiplier * winFactor(grid, factors, win.positions);   // x6 when both are in it

A win that touches no multiplier is unchanged, and a second factor landing on the same cell multiplies with the first.

Splits count without occupying

A split symbol counts twice in its column, which doubles that column's ways. pay-ways counts cells and cannot know a cell means two, so the counting lives here. A symbol that occupies the cells it counts is a block instead, and that is big-symbols.

import { splits, waysWithSplits } from "@open-rgs/symbols";

const COUNTS = splits([["A2", 2], ["A3", 3]]);

waysWithSplits(grid, (s) => s === "A" || s === "A2", COUNTS);   // { ways, columns }