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