@open-rgs/ext-holdwin
v0.1.0
Published
Hold-&-Win (respin / lock-&-spin) engine for open-rgs Lua maths. Coins, collectors, jackpot tiers, max-win cap — data-driven.
Maintainers
Readme
@open-rgs/ext-holdwin
A hold-&-win (respin / lock-&-spin) engine for open-rgs Lua maths. Cash coins, collectors, jackpot tiers, reset-on-land respins, and a max-win cap — all data-driven, currency-blind.
It's the companion to @open-rgs/ext-reels: reels gives you spins + paylines; hold-&-win gives you the bonus that defines the genre.
Install
bun add @open-rgs/ext-holdwinWire it in
import { holdwin } from "@open-rgs/ext-holdwin";
import { loadLuaMath } from "@open-rgs/core";
const math = await loadLuaMath("./maths/spin.lua", { extensions: [holdwin] });holdwin ships as a prelude transform: the toolkit is injected as a holdwin local at the top of your math chunk, so you use it directly — no require(). (This is deliberate: a require()'d module is referenced across the JS bridge, which marshals nested Lua tables — grids, paytables — unreliably. The prelude keeps everything native.)
The easy path — holdwin.engine{}
A canonical hold-&-win game is just a config table:
local play = holdwin.engine({
reelsets = { REELSET }, -- one is picked uniformly per spin
lines = LINES, -- { {1,1,1}, {2,2,2}, ... }
paytable = PAY, -- { SYM = { [3] = pay, ... } }
wild = "W",
coin_face = "C", -- base-grid coin symbol
coin_value = function(rng) ... end, -- value of a base coin
trigger_count = 6, -- coins to start the feature
respins = 3,
respin_coin = function(rng) ... end, -- respin coin value (0/nil = empty; jackpots included)
is_jackpot = function(v) return v >= 25 end,
collectors = 1, -- collector slots during the feature
maxWin = 6000,
})
return { kind = "simple", name = "my-holdwin", version = "1.0.0", rtp = 0.96, play = play }The flexible path — primitives
When your game has multiple feature routes, alternate board seedings, or a jackpot wheel, compose the primitives directly. (See the scarlet-pact-hold-and-win game for a full worked example.)
| Function | Purpose |
|---|---|
| holdwin.cumulative(weights) / pick_index(cum, rng) | weighted sampling by integer ratios |
| holdwin.uniform(n, rng) | uniform integer in [0, n) |
| holdwin.sampler(values, weights) | reusable fn(rng) -> value |
| holdwin.spin(reelset, rng) → stops | one uniform stop per reel |
| holdwin.grid(stops) → grid | column-major grid[col][row] |
| holdwin.count(grid, face) / positions(grid, face) | find symbols |
| holdwin.eval_line / eval_lines(grid, lines, paytable, wild) | payline wins (wild substitutes; all-wild pays the wild's entry; non-paytable symbols block a line) |
| holdwin.cap(payout, maxWin) → (capped, hit?) | max-win clamp |
| holdwin.collect(collectors, amount) | add to every present collector, return total |
| holdwin.maybe_add_collector(collectors, rng, interval, maxC) | probabilistically place a collector |
| holdwin.respins(o) | the respin driver — reset-on-land, per-collector accumulation, jackpot one-shot, max-win cap |
The respin model
A collectors table is a fixed array of slots: -1 = empty, >= 0 = a collector holding that much. Each respin re-rolls coins in the eligible cells; every present collector rakes the spin's coin sum; the respin count resets whenever something new lands and ticks down when nothing does. The feature pays the sum of all collectors, capped at maxWin. A classic "coins lock and sum" game is just the single-permanent-collector case.
RNG & marks
Outcomes come from host.rng_next() (the injected, auditable CSPRNG). For simulator RTP attribution call host.mark.contribute(bucket, multiplier) / host.mark.tag(name) / host.mark.count(name) in your math.
License
MIT
