@open-rgs/ext-reels 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/meta/reels.d.lua +128 -0
- package/package.json +36 -0
- package/src/index.ts +59 -0
- package/src/reels.lua +280 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 open-rgs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @open-rgs/ext-reels
|
|
2
|
+
|
|
3
|
+
Reel-spin utilities for open-rgs Lua maths. The reference implementation
|
|
4
|
+
of the [`LuaExtension`](https://github.com/open-rgs/open-rgs/blob/main/packages/contract/src/index.ts)
|
|
5
|
+
contract — use it directly, or copy the shape to ship your own.
|
|
6
|
+
|
|
7
|
+
## What's in it
|
|
8
|
+
|
|
9
|
+
- A pure-Lua module (`reels.lua`) that math files `require("reels")` for:
|
|
10
|
+
`spin`, `spin_column`, `symbol_at`, `count_symbol`, `positions_of`,
|
|
11
|
+
`line_symbols`.
|
|
12
|
+
- A native helper, `weighted_pick(weights, r)`, for hot paths where
|
|
13
|
+
iterating a big bucket list in Lua is slower than doing it in TS
|
|
14
|
+
(mostly relevant inside simulator loops, not normal in-game spins).
|
|
15
|
+
- Lua-language-server annotations in `meta/reels.d.lua` so your editor
|
|
16
|
+
knows the types on the Lua side.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add @open-rgs/ext-reels
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Wire into a game
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { loadLuaMath } from "@open-rgs/core";
|
|
28
|
+
import { reels } from "@open-rgs/ext-reels";
|
|
29
|
+
|
|
30
|
+
const math = await loadLuaMath("./maths/spin.lua", {
|
|
31
|
+
extensions: [reels],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then in Lua:
|
|
36
|
+
|
|
37
|
+
```lua
|
|
38
|
+
local r = require("reels")
|
|
39
|
+
|
|
40
|
+
local STRIPS = {
|
|
41
|
+
{"A","K","Q","J","T","S","A","K","S","Q"},
|
|
42
|
+
{"K","Q","A","S","T","J","S","A","Q","K"},
|
|
43
|
+
-- …
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
kind = "simple", name = "demo", version = "0.1.0", rtp = 0.95,
|
|
48
|
+
play = function(prev, ctx)
|
|
49
|
+
local grid = r.spin(STRIPS, 3, host.rng_next)
|
|
50
|
+
local scatters = r.count_symbol(grid, "S")
|
|
51
|
+
|
|
52
|
+
local mult = 0
|
|
53
|
+
if scatters >= 3 then mult = 5 end
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
multiplier = mult,
|
|
57
|
+
ops = {
|
|
58
|
+
{ kind = "grid", cells = grid },
|
|
59
|
+
{ kind = "scatter", count = scatters },
|
|
60
|
+
},
|
|
61
|
+
type = mult > 0 and "trigger" or "spin",
|
|
62
|
+
}
|
|
63
|
+
end,
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Editor types on the Lua side
|
|
68
|
+
|
|
69
|
+
Add this to your game's `.luarc.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"workspace.library": ["./node_modules/@open-rgs/ext-reels/meta"]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You get completion + type-checking on `r.spin`, `r.count_symbol`, etc.
|
|
78
|
+
|
|
79
|
+
## Test it
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bun install
|
|
83
|
+
bun test
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The smoke test spins up a real Lua VM, registers the extension, and
|
|
87
|
+
verifies a deterministic grid against a known RNG seed.
|
|
88
|
+
|
|
89
|
+
## Build your own extension
|
|
90
|
+
|
|
91
|
+
The shape is two-and-a-half things:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import type { LuaExtension } from "@open-rgs/contract";
|
|
95
|
+
|
|
96
|
+
export const ext: LuaExtension = {
|
|
97
|
+
name: "your-module", // require("your-module") in Lua
|
|
98
|
+
version: "0.1.0",
|
|
99
|
+
lua: `-- optional pure-Lua source; should return a table
|
|
100
|
+
local M = {}
|
|
101
|
+
function M.hello() return "hi" end
|
|
102
|
+
return M`,
|
|
103
|
+
host: (vm) => ({
|
|
104
|
+
// optional TS-backed functions; shadow same-named lua keys
|
|
105
|
+
fast_thing(x: number) { return x * 2; },
|
|
106
|
+
}),
|
|
107
|
+
transform: (src, path) => src, // optional preprocessor
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Then publish under `@your-scope/<name>` and document the require() shape.
|
|
112
|
+
That's it.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT.
|
package/meta/reels.d.lua
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---@meta
|
|
2
|
+
--
|
|
3
|
+
-- lua-language-server type annotations for @open-rgs/ext-reels.
|
|
4
|
+
-- Add this file's directory to the workspace.library setting in
|
|
5
|
+
-- .luarc.json to get editor completion and type-checking on the
|
|
6
|
+
-- Lua side.
|
|
7
|
+
|
|
8
|
+
---@alias reels.Symbol string
|
|
9
|
+
---@alias reels.Strip reels.Symbol[]
|
|
10
|
+
---@alias reels.Grid reels.Strip[]
|
|
11
|
+
---@alias reels.Pos integer[]
|
|
12
|
+
---@alias reels.Line integer[]
|
|
13
|
+
---@alias reels.Rng fun(): number
|
|
14
|
+
|
|
15
|
+
---@class reels.LineWin
|
|
16
|
+
---@field symbol reels.Symbol
|
|
17
|
+
---@field count integer
|
|
18
|
+
---@field payout number
|
|
19
|
+
---@field wild_mult number multiplier applied from wilds on the line
|
|
20
|
+
|
|
21
|
+
---@class reels.PaylineWin : reels.LineWin
|
|
22
|
+
---@field line_index integer
|
|
23
|
+
|
|
24
|
+
---@class reels.PaylinesResult
|
|
25
|
+
---@field total number
|
|
26
|
+
---@field lines reels.PaylineWin[]
|
|
27
|
+
|
|
28
|
+
---@alias reels.Paytable table<reels.Symbol, table<integer, number>>
|
|
29
|
+
---@alias reels.ScatterPaytable table<integer, number>
|
|
30
|
+
---@alias reels.WildMultGrid table<integer, table<integer, number>>
|
|
31
|
+
|
|
32
|
+
---@class reels
|
|
33
|
+
local M = {}
|
|
34
|
+
|
|
35
|
+
---Spin a single column. Returns `rows` symbols starting from a uniform
|
|
36
|
+
---random offset, wrapping at the strip end.
|
|
37
|
+
---@param strip reels.Strip
|
|
38
|
+
---@param rows integer
|
|
39
|
+
---@param rng reels.Rng
|
|
40
|
+
---@return reels.Strip
|
|
41
|
+
function M.spin_column(strip, rows, rng) end
|
|
42
|
+
|
|
43
|
+
---Spin a whole reelset.
|
|
44
|
+
---@param strips reels.Strip[]
|
|
45
|
+
---@param rows integer
|
|
46
|
+
---@param rng reels.Rng
|
|
47
|
+
---@return reels.Grid
|
|
48
|
+
function M.spin(strips, rows, rng) end
|
|
49
|
+
|
|
50
|
+
---Get the symbol at a {col, row} position. nil if out of range.
|
|
51
|
+
---@param grid reels.Grid
|
|
52
|
+
---@param pos reels.Pos
|
|
53
|
+
---@return reels.Symbol?
|
|
54
|
+
function M.symbol_at(grid, pos) end
|
|
55
|
+
|
|
56
|
+
---Count occurrences of `symbol` across the entire grid.
|
|
57
|
+
---@param grid reels.Grid
|
|
58
|
+
---@param symbol reels.Symbol
|
|
59
|
+
---@return integer
|
|
60
|
+
function M.count_symbol(grid, symbol) end
|
|
61
|
+
|
|
62
|
+
---Find every {col, row} where `symbol` appears.
|
|
63
|
+
---@param grid reels.Grid
|
|
64
|
+
---@param symbol reels.Symbol
|
|
65
|
+
---@return reels.Pos[]
|
|
66
|
+
function M.positions_of(grid, symbol) end
|
|
67
|
+
|
|
68
|
+
---Read the column-by-column symbols along a payline.
|
|
69
|
+
---@param grid reels.Grid
|
|
70
|
+
---@param line reels.Line per-column row indices
|
|
71
|
+
---@return reels.Symbol[]
|
|
72
|
+
function M.line_symbols(grid, line) end
|
|
73
|
+
|
|
74
|
+
---Evaluate ONE payline left-to-right. Wilds substitute for any symbol
|
|
75
|
+
---except scatter; per-position wild multipliers compound across the
|
|
76
|
+
---winning run. Returns nil on no-win.
|
|
77
|
+
---@param symbols reels.Symbol[]
|
|
78
|
+
---@param paytable reels.Paytable
|
|
79
|
+
---@param wild_id reels.Symbol
|
|
80
|
+
---@param scatter_id reels.Symbol
|
|
81
|
+
---@param wild_mults table<integer, number>?
|
|
82
|
+
---@return reels.LineWin?
|
|
83
|
+
function M.eval_payline_left(symbols, paytable, wild_id, scatter_id, wild_mults) end
|
|
84
|
+
|
|
85
|
+
---Evaluate every payline against the grid, return total + per-line winners.
|
|
86
|
+
---@param grid reels.Grid
|
|
87
|
+
---@param paylines reels.Line[]
|
|
88
|
+
---@param paytable reels.Paytable
|
|
89
|
+
---@param wild_id reels.Symbol
|
|
90
|
+
---@param scatter_id reels.Symbol
|
|
91
|
+
---@param wild_mult_grid reels.WildMultGrid?
|
|
92
|
+
---@return reels.PaylinesResult
|
|
93
|
+
function M.eval_paylines(grid, paylines, paytable, wild_id, scatter_id, wild_mult_grid) end
|
|
94
|
+
|
|
95
|
+
---Book-of mechanic: fill every reel containing `symbol` with `symbol`
|
|
96
|
+
---on all rows. Mutates and returns `grid` for chaining.
|
|
97
|
+
---@param grid reels.Grid
|
|
98
|
+
---@param symbol reels.Symbol
|
|
99
|
+
---@return reels.Grid
|
|
100
|
+
function M.expand_symbol(grid, symbol) end
|
|
101
|
+
|
|
102
|
+
---Pick a string key from a name→weight map proportional to weights.
|
|
103
|
+
---@param weights table<string, number>
|
|
104
|
+
---@param rng reels.Rng
|
|
105
|
+
---@return string?
|
|
106
|
+
function M.pick_weighted_key(weights, rng) end
|
|
107
|
+
|
|
108
|
+
---Scatter pay lookup. `count` >= the smallest key with a non-zero value triggers.
|
|
109
|
+
---@param count integer
|
|
110
|
+
---@param scatter_pays reels.ScatterPaytable
|
|
111
|
+
---@return number
|
|
112
|
+
function M.count_scatter_pay(count, scatter_pays) end
|
|
113
|
+
|
|
114
|
+
---10 classic 5×3 paylines (rows then diagonals then zigzags).
|
|
115
|
+
---@type reels.Line[]
|
|
116
|
+
M.PAYLINES_5x3_10 = {}
|
|
117
|
+
|
|
118
|
+
---20-line 5×3 set (10 classic + 10 alternating).
|
|
119
|
+
---@type reels.Line[]
|
|
120
|
+
M.PAYLINES_5x3_20 = {}
|
|
121
|
+
|
|
122
|
+
---Native: pick a 1..#weights index with probability ∝ weights[i]. `r` ∈ [0,1).
|
|
123
|
+
---@param weights number[]
|
|
124
|
+
---@param r number
|
|
125
|
+
---@return integer
|
|
126
|
+
function M.weighted_pick(weights, r) end
|
|
127
|
+
|
|
128
|
+
return M
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-rgs/ext-reels",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Reel-spin utilities for open-rgs Lua maths. Reference LuaExtension implementation.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/open-rgs/ext-reels.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/open-rgs/ext-reels",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "src/index.ts",
|
|
13
|
+
"types": "src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"provenance": true
|
|
20
|
+
},
|
|
21
|
+
"files": ["src", "meta", "README.md"],
|
|
22
|
+
"keywords": ["open-rgs", "lua-extension", "rgs", "slot", "reels"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@open-rgs/contract": "^0.3.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@open-rgs/contract": "^0.3.0",
|
|
32
|
+
"@open-rgs/core": "^0.5.0",
|
|
33
|
+
"@types/bun": "latest",
|
|
34
|
+
"typescript": "^5.4.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @open-rgs/ext-reels
|
|
2
|
+
//
|
|
3
|
+
// Reference LuaExtension for open-rgs. Adds a `reels` module the math
|
|
4
|
+
// can `require("reels")` to get reel-spin utilities.
|
|
5
|
+
//
|
|
6
|
+
// Two parts:
|
|
7
|
+
// • A pure-Lua module (src/reels.lua) with spin / symbol_at / count /
|
|
8
|
+
// positions_of / line_symbols. Editor-friendly via meta/reels.d.lua.
|
|
9
|
+
// • A native helper (weighted_pick) for cases where Lua iteration is
|
|
10
|
+
// slow enough to matter — e.g. running an exhaustive RTP simulator.
|
|
11
|
+
//
|
|
12
|
+
// Wiring (in the integrator's boot file):
|
|
13
|
+
//
|
|
14
|
+
// import { reels } from "@open-rgs/ext-reels";
|
|
15
|
+
// const math = await loadLuaMath("./maths/spin.lua", {
|
|
16
|
+
// extensions: [reels],
|
|
17
|
+
// });
|
|
18
|
+
//
|
|
19
|
+
// Lua-side usage:
|
|
20
|
+
//
|
|
21
|
+
// local r = require("reels")
|
|
22
|
+
// local grid = r.spin(STRIPS, 3, host.rng_next)
|
|
23
|
+
// local scatters = r.count_symbol(grid, "S")
|
|
24
|
+
|
|
25
|
+
import type { LuaExtension } from "@open-rgs/contract";
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const luaSource = readFileSync(join(here, "reels.lua"), "utf-8");
|
|
32
|
+
|
|
33
|
+
export const reels: LuaExtension = {
|
|
34
|
+
name: "reels",
|
|
35
|
+
version: "0.1.0",
|
|
36
|
+
lua: luaSource,
|
|
37
|
+
host: () => ({
|
|
38
|
+
/** Pick an index 1..weights.length with probability proportional
|
|
39
|
+
* to each weight. `r` ∈ [0, 1) selects the slot.
|
|
40
|
+
*
|
|
41
|
+
* Faster than the equivalent Lua loop for big buckets — useful
|
|
42
|
+
* inside hot simulator loops; for normal in-game spins the Lua
|
|
43
|
+
* side is fine. */
|
|
44
|
+
weighted_pick(weights: readonly number[], r: number): number {
|
|
45
|
+
let total = 0;
|
|
46
|
+
for (const w of weights) total += w;
|
|
47
|
+
if (total <= 0) return 1;
|
|
48
|
+
const target = r * total;
|
|
49
|
+
let acc = 0;
|
|
50
|
+
for (let i = 0; i < weights.length; i++) {
|
|
51
|
+
acc += weights[i]!;
|
|
52
|
+
if (target < acc) return i + 1; // 1-indexed for Lua
|
|
53
|
+
}
|
|
54
|
+
return weights.length;
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default reels;
|
package/src/reels.lua
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
-- @open-rgs/ext-reels — reel-spin utilities for open-rgs Lua maths.
|
|
2
|
+
--
|
|
3
|
+
-- Conventions:
|
|
4
|
+
-- • Strips are column-major: strips[col][index] (1-indexed)
|
|
5
|
+
-- • Grids are column-major: grid[col][row] (1-indexed)
|
|
6
|
+
-- • Positions are { col, row }
|
|
7
|
+
-- • Symbols are strings; nothing else is opaque
|
|
8
|
+
-- • Paylines are arrays of row indices (one per column)
|
|
9
|
+
|
|
10
|
+
local M = {}
|
|
11
|
+
|
|
12
|
+
-- ─── Spin primitives ───────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
-- Spin one column: pick a starting offset uniformly, return `rows` symbols
|
|
15
|
+
-- starting at that offset (wrapping the strip end → start). `rng` is a
|
|
16
|
+
-- function returning a float in [0, 1).
|
|
17
|
+
function M.spin_column(strip, rows, rng)
|
|
18
|
+
local n = #strip
|
|
19
|
+
local start = math.floor(rng() * n)
|
|
20
|
+
local out = {}
|
|
21
|
+
for r = 1, rows do
|
|
22
|
+
out[r] = strip[((start + r - 1) % n) + 1]
|
|
23
|
+
end
|
|
24
|
+
return out
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
-- Spin a whole reelset. `strips` is the per-column strip table.
|
|
28
|
+
function M.spin(strips, rows, rng)
|
|
29
|
+
local cols = #strips
|
|
30
|
+
local grid = {}
|
|
31
|
+
for c = 1, cols do
|
|
32
|
+
grid[c] = M.spin_column(strips[c], rows, rng)
|
|
33
|
+
end
|
|
34
|
+
return grid
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
-- ─── Grid lookups ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
-- Lookup the symbol at a {col, row} position. Returns nil if out of range.
|
|
40
|
+
function M.symbol_at(grid, pos)
|
|
41
|
+
local col = grid[pos[1]]
|
|
42
|
+
if not col then return nil end
|
|
43
|
+
return col[pos[2]]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
-- Count how many times `symbol` appears across the whole grid.
|
|
47
|
+
function M.count_symbol(grid, symbol)
|
|
48
|
+
local n = 0
|
|
49
|
+
for c = 1, #grid do
|
|
50
|
+
local col = grid[c]
|
|
51
|
+
for r = 1, #col do
|
|
52
|
+
if col[r] == symbol then n = n + 1 end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
return n
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
-- Find every {col, row} position where `symbol` appears.
|
|
59
|
+
function M.positions_of(grid, symbol)
|
|
60
|
+
local out = {}
|
|
61
|
+
for c = 1, #grid do
|
|
62
|
+
local col = grid[c]
|
|
63
|
+
for r = 1, #col do
|
|
64
|
+
if col[r] == symbol then
|
|
65
|
+
out[#out + 1] = { c, r }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
return out
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
-- Read the symbols along a payline. `line` is a per-column row index.
|
|
73
|
+
-- Returns the list of symbols column-by-column.
|
|
74
|
+
function M.line_symbols(grid, line)
|
|
75
|
+
local out = {}
|
|
76
|
+
for c = 1, #line do
|
|
77
|
+
out[c] = grid[c][line[c]]
|
|
78
|
+
end
|
|
79
|
+
return out
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
-- ─── Payline evaluation ───────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
-- Evaluate ONE payline left-to-right.
|
|
85
|
+
--
|
|
86
|
+
-- A "win" is the longest prefix of matching symbols starting from the
|
|
87
|
+
-- leftmost reel. Wilds substitute for any symbol except scatter. If the
|
|
88
|
+
-- entire prefix is wilds, the run is treated as "wild" itself (paid
|
|
89
|
+
-- via paytable[wild_id] if present, otherwise no pay).
|
|
90
|
+
--
|
|
91
|
+
-- Per-position wild multipliers compound on the winning run only.
|
|
92
|
+
--
|
|
93
|
+
-- Args:
|
|
94
|
+
-- symbols : array of symbols along the line (one per column)
|
|
95
|
+
-- paytable : { [symbol] = { [count] = mult, ... }, ... }
|
|
96
|
+
-- count is the number of matching symbols starting at left.
|
|
97
|
+
-- wild_id : symbol id that substitutes for non-scatter symbols
|
|
98
|
+
-- scatter_id : symbol id that NEVER substitutes (skip from line pays)
|
|
99
|
+
-- wild_mults : optional { [col_index] = mult } — multiplier carried by
|
|
100
|
+
-- the wild at that position. Compounds across all wilds
|
|
101
|
+
-- that participate in the winning run.
|
|
102
|
+
--
|
|
103
|
+
-- Returns: { symbol, count, payout, wild_mult } or nil on no-win.
|
|
104
|
+
function M.eval_payline_left(symbols, paytable, wild_id, scatter_id, wild_mults)
|
|
105
|
+
local n = #symbols
|
|
106
|
+
if n == 0 then return nil end
|
|
107
|
+
|
|
108
|
+
-- Anchor: first non-wild symbol from the left. If everything is wild,
|
|
109
|
+
-- anchor is the wild itself (so the wild's own paytable entry, if any,
|
|
110
|
+
-- determines the prize for an all-wild line).
|
|
111
|
+
local anchor
|
|
112
|
+
for i = 1, n do
|
|
113
|
+
if symbols[i] ~= wild_id then
|
|
114
|
+
anchor = symbols[i]
|
|
115
|
+
break
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
if anchor == nil then anchor = wild_id end
|
|
119
|
+
|
|
120
|
+
-- Scatters never pay by line — only by count.
|
|
121
|
+
if anchor == scatter_id then return nil end
|
|
122
|
+
|
|
123
|
+
-- Walk the prefix: count {anchor, wild} matches, compound wild mult.
|
|
124
|
+
local run_count = 0
|
|
125
|
+
local wild_mult = 1
|
|
126
|
+
for i = 1, n do
|
|
127
|
+
local s = symbols[i]
|
|
128
|
+
if s == anchor or s == wild_id then
|
|
129
|
+
run_count = run_count + 1
|
|
130
|
+
if s == wild_id and wild_mults and wild_mults[i] and wild_mults[i] > 0 then
|
|
131
|
+
wild_mult = wild_mult * wild_mults[i]
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
break
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
local pt = paytable[anchor]
|
|
139
|
+
if not pt then return nil end
|
|
140
|
+
local base = pt[run_count] or 0
|
|
141
|
+
if base == 0 then return nil end
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
symbol = anchor,
|
|
145
|
+
count = run_count,
|
|
146
|
+
payout = base * wild_mult,
|
|
147
|
+
wild_mult = wild_mult,
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
-- Evaluate every payline against `grid`. Returns the total payout (sum
|
|
152
|
+
-- of all line wins) and a per-line breakdown of every winning line.
|
|
153
|
+
--
|
|
154
|
+
-- `wild_mult_grid` is OPTIONAL: { [col] = { [row] = mult } }. Lets the
|
|
155
|
+
-- math attach a multiplier to specific wild landings on the grid, which
|
|
156
|
+
-- the per-line evaluator picks up when a wild participates in a winning
|
|
157
|
+
-- run. Pass nil if your wilds carry no multiplier.
|
|
158
|
+
function M.eval_paylines(grid, paylines, paytable, wild_id, scatter_id, wild_mult_grid)
|
|
159
|
+
local total = 0
|
|
160
|
+
local lines = {}
|
|
161
|
+
for li = 1, #paylines do
|
|
162
|
+
local line = paylines[li]
|
|
163
|
+
local row_syms = {}
|
|
164
|
+
local row_mults
|
|
165
|
+
for c = 1, #line do
|
|
166
|
+
row_syms[c] = grid[c][line[c]]
|
|
167
|
+
if wild_mult_grid and wild_mult_grid[c] then
|
|
168
|
+
local m = wild_mult_grid[c][line[c]]
|
|
169
|
+
if m and m > 0 then
|
|
170
|
+
row_mults = row_mults or {}
|
|
171
|
+
row_mults[c] = m
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
local r = M.eval_payline_left(row_syms, paytable, wild_id, scatter_id, row_mults)
|
|
176
|
+
if r then
|
|
177
|
+
total = total + r.payout
|
|
178
|
+
lines[#lines + 1] = {
|
|
179
|
+
line_index = li,
|
|
180
|
+
symbol = r.symbol,
|
|
181
|
+
count = r.count,
|
|
182
|
+
payout = r.payout,
|
|
183
|
+
wild_mult = r.wild_mult,
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
return { total = total, lines = lines }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
-- ─── Book-of mechanic ─────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
-- Expand a symbol across every column that contains it: any reel with
|
|
193
|
+
-- at least one landing of `symbol` is filled with `symbol` on all rows.
|
|
194
|
+
-- Mutates AND returns the grid for chaining convenience.
|
|
195
|
+
function M.expand_symbol(grid, symbol)
|
|
196
|
+
for c = 1, #grid do
|
|
197
|
+
local col = grid[c]
|
|
198
|
+
local has = false
|
|
199
|
+
for r = 1, #col do
|
|
200
|
+
if col[r] == symbol then has = true; break end
|
|
201
|
+
end
|
|
202
|
+
if has then
|
|
203
|
+
for r = 1, #col do
|
|
204
|
+
col[r] = symbol
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
return grid
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
-- ─── Weighted picking ─────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
-- Pick a string key from a name→weight map proportional to weights.
|
|
214
|
+
-- Iteration order of pairs() is implementation-defined; for repeatable
|
|
215
|
+
-- results across runs, sort the keys upstream and call weighted_pick.
|
|
216
|
+
function M.pick_weighted_key(weights, rng)
|
|
217
|
+
local total = 0
|
|
218
|
+
for _, w in pairs(weights) do total = total + w end
|
|
219
|
+
if total <= 0 then return nil end
|
|
220
|
+
local target = rng() * total
|
|
221
|
+
local acc = 0
|
|
222
|
+
local last_k
|
|
223
|
+
for k, w in pairs(weights) do
|
|
224
|
+
last_k = k
|
|
225
|
+
acc = acc + w
|
|
226
|
+
if target < acc then return k end
|
|
227
|
+
end
|
|
228
|
+
return last_k
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
-- ─── Scatter pay lookup ───────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
-- Tiny helper kept here for parity with line evaluator: scatters pay
|
|
234
|
+
-- by count (3+ usually), independent of position.
|
|
235
|
+
function M.count_scatter_pay(count, scatter_pays)
|
|
236
|
+
return scatter_pays[count] or 0
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
-- ─── Standard payline shapes ──────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
-- 10 classic paylines for a 5×3 grid. Row 1 = top.
|
|
242
|
+
-- Order matches industry convention (rows first, then diagonals, zigzags).
|
|
243
|
+
M.PAYLINES_5x3_10 = {
|
|
244
|
+
{ 2, 2, 2, 2, 2 }, -- 1: middle row
|
|
245
|
+
{ 1, 1, 1, 1, 1 }, -- 2: top row
|
|
246
|
+
{ 3, 3, 3, 3, 3 }, -- 3: bottom row
|
|
247
|
+
{ 1, 2, 3, 2, 1 }, -- 4: V down-up
|
|
248
|
+
{ 3, 2, 1, 2, 3 }, -- 5: V up-down
|
|
249
|
+
{ 1, 1, 2, 3, 3 }, -- 6: top→bottom diag
|
|
250
|
+
{ 3, 3, 2, 1, 1 }, -- 7: bottom→top diag
|
|
251
|
+
{ 2, 1, 1, 1, 2 }, -- 8: middle, top hump
|
|
252
|
+
{ 2, 3, 3, 3, 2 }, -- 9: middle, bottom hump
|
|
253
|
+
{ 1, 2, 1, 2, 1 }, -- 10: zigzag top
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
-- 20-line set (10 above + 10 more).
|
|
257
|
+
M.PAYLINES_5x3_20 = {
|
|
258
|
+
{ 2, 2, 2, 2, 2 },
|
|
259
|
+
{ 1, 1, 1, 1, 1 },
|
|
260
|
+
{ 3, 3, 3, 3, 3 },
|
|
261
|
+
{ 1, 2, 3, 2, 1 },
|
|
262
|
+
{ 3, 2, 1, 2, 3 },
|
|
263
|
+
{ 1, 1, 2, 3, 3 },
|
|
264
|
+
{ 3, 3, 2, 1, 1 },
|
|
265
|
+
{ 2, 1, 1, 1, 2 },
|
|
266
|
+
{ 2, 3, 3, 3, 2 },
|
|
267
|
+
{ 1, 2, 1, 2, 1 },
|
|
268
|
+
{ 3, 2, 3, 2, 3 },
|
|
269
|
+
{ 2, 1, 2, 1, 2 },
|
|
270
|
+
{ 2, 3, 2, 3, 2 },
|
|
271
|
+
{ 1, 1, 2, 1, 1 },
|
|
272
|
+
{ 3, 3, 2, 3, 3 },
|
|
273
|
+
{ 2, 2, 1, 2, 2 },
|
|
274
|
+
{ 2, 2, 3, 2, 2 },
|
|
275
|
+
{ 1, 2, 2, 2, 1 },
|
|
276
|
+
{ 3, 2, 2, 2, 3 },
|
|
277
|
+
{ 1, 3, 1, 3, 1 },
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return M
|