@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.
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.
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 }