@open-rgs/strips
A fixed sequence, a stop, and the window it shows.
The rest of these libraries prefer weighted sets, because a weight is a probability you can read and a strip is one you have to count for. That preference is real. It is also not a reason to pretend strips do not exist.
When a strip is the right answer
Three cases, and if none of them applies to a new game, use weights.
- A port of an existing game has strips. Reproducing its math means reproducing them, not re-deriving equivalent weights and hoping.
- Some jurisdictions and labs ask for the strip listing itself.
- Adjacency is a strip's whole point. What can appear above what is fixed by the sequence, so stacks and near-misses are properties of the reel rather than something a per-cell draw has to be talked into.
A spin is a stop per reel
The window reads down from the stop and wraps, because a reel is a loop. A spin returns the stops as well as the board: the stops are the smaller and more useful record, and a report that keeps only the board has thrown the replay away.
import { spinStrips, boardFrom, windowAt } from "@open-rgs/strips";
import { rect } from "@open-rgs/grid";
const REELS = [
["A", "W", "W", "W", "B", "C", "A", "B"], // one 3-tall W stack
["A", "B", "C", "W", "B", "A", "C", "B"],
["B", "A", "C", "B", "W", "A", "B", "C"],
];
const { grid, stops } = spinStrips(REELS, rect(3, 3), host.rng_next);
// or replay one from its stops alone
boardFrom(REELS, rect(3, 3), stops); A window taller than its own reel is refused at build time, because it would show the same symbol twice in one column and quietly change every adjacency the strip was written for.
The counting a weighted set gives you free
This is the part that makes strips workable rather than a leap of faith. Every figure is exact, because a strip is a finite object and its answers are arithmetic rather than simulation results.
import { cellProbability, windowProbability, expectedInWindow,
longestRun, windowCountDistribution } from "@open-rgs/strips";
cellProbability(reel, "W"); // 3/8, the chance any one row shows it
windowProbability(reel, "W", 3); // 5/8, at least one in a 3-row window
expectedInWindow(reel, "W", 3); // 9/8, the expected count
longestRun(reel, "W"); // 3, how tall the stack can be
windowCountDistribution(reel, "W", 3); // [.375, .25, .25, .125] by count The trap this exists to close. At-least-one in a window is not the row chance times the height. On the reel above that formula gives 1.125, which is not even a probability: a stacked symbol appears in several rows of the same window, so multiplying overcounts exactly where a strip game puts its stacks. The expected count is height times the row chance, and that one is exact. Two similar-sounding numbers, one of which is wrong, which is why both are here rather than left to the reader.
Building a strip from a listing
For a new game this is a weighted set with extra steps, and the honest advice is to use one. It is here for the case where a specification names the contents of a reel but not their order.
import { stripOf } from "@open-rgs/strips";
const reel = stripOf({ LOW: 12, MID: 6, HIGH: 3, WILD: 1 }, host.rng_next); The order comes from the stream, so it replays from a seed like everything else. What it cannot give you is adjacency: if the stacks matter, write the sequence.