434 lines
20 KiB
JavaScript
434 lines
20 KiB
JavaScript
// Slot machine definitions — pure data, no Phaser imports.
|
||
//
|
||
// Reel strips are written compressed: each entry is either a symbol id string
|
||
// (one copy) or [symbolId, count]. expandStrip() flattens to the real strip.
|
||
// Symbol weighting comes entirely from repetition counts, which keeps the
|
||
// paytable math verifiable by Monte-Carlo (server/scripts/verifySlots.js).
|
||
//
|
||
// Money model:
|
||
// - betLevels are TOTAL bet per spin in credits (shared casino bankroll).
|
||
// - 'lines' machines: paytable values are multipliers of the LINE bet
|
||
// (total bet / paylines.length). Per-win credits = round(pay * wildMult * bet / nLines).
|
||
// - 'scatterPays' machines: paytable tiers are multipliers of the TOTAL bet.
|
||
// - scatter.pays and pickBonus/holdSpin coin values are multipliers of the TOTAL bet.
|
||
|
||
export function expandStrip(compressed) {
|
||
const out = [];
|
||
for (const entry of compressed) {
|
||
if (Array.isArray(entry)) {
|
||
const [sym, count] = entry;
|
||
for (let i = 0; i < count; i++) out.push(sym);
|
||
} else out.push(entry);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Spread each symbol's copies evenly around the strip (deterministic, no rng)
|
||
// so spinning reels never show cheap-looking blocks of identical symbols —
|
||
// EXCEPT where an entry asks for blocks: [sym, count, blockSize] places the
|
||
// copies as runs of blockSize consecutive slots (stacked coins / candy clumps),
|
||
// which is what lets a 3-row window show multiple copies on one reel.
|
||
export function interleaveStrip(compressed) {
|
||
const entries = compressed.map((e) => (Array.isArray(e) ? e : [e, 1]));
|
||
const len = entries.reduce((s, [, c]) => s + c, 0);
|
||
const strip = new Array(len).fill(null);
|
||
const sorted = entries.slice().sort((a, b) => b[1] - a[1]);
|
||
sorted.forEach(([sym, count, block = 1], si) => {
|
||
const nChunks = Math.ceil(count / block);
|
||
const step = len / nChunks;
|
||
let remaining = count;
|
||
for (let i = 0; i < nChunks; i++) {
|
||
let idx = Math.floor(i * step + step / 2 + si) % len;
|
||
const size = Math.min(block, remaining);
|
||
remaining -= size;
|
||
for (let j = 0; j < size; j++) {
|
||
while (strip[idx] !== null) idx = (idx + 1) % len;
|
||
strip[idx] = sym;
|
||
idx = (idx + 1) % len;
|
||
}
|
||
}
|
||
});
|
||
return strip;
|
||
}
|
||
|
||
// Standard 20-payline set for 5×3 video slots (row index per reel, 0 = top).
|
||
export const LINES_20 = [
|
||
[1, 1, 1, 1, 1], [0, 0, 0, 0, 0], [2, 2, 2, 2, 2], [0, 1, 2, 1, 0], [2, 1, 0, 1, 2],
|
||
[1, 0, 0, 0, 1], [1, 2, 2, 2, 1], [0, 0, 1, 2, 2], [2, 2, 1, 0, 0], [1, 2, 1, 0, 1],
|
||
[1, 0, 1, 2, 1], [0, 1, 1, 1, 0], [2, 1, 1, 1, 2], [0, 1, 0, 1, 0], [2, 1, 2, 1, 2],
|
||
[1, 1, 0, 1, 1], [1, 1, 2, 1, 1], [0, 2, 0, 2, 0], [2, 0, 2, 0, 2], [0, 2, 2, 2, 0],
|
||
];
|
||
|
||
// 5 paylines for a 3×3 window: three rows + two diagonals.
|
||
export const LINES_5 = [
|
||
[1, 1, 1], [0, 0, 0], [2, 2, 2], [0, 1, 2], [2, 1, 0],
|
||
];
|
||
|
||
export const MACHINES = [
|
||
// ── 1. Liberty Belle — 1899 mechanical, pull lever, single line ─────────────
|
||
{
|
||
id: 'liberty-belle',
|
||
name: 'Liberty Belle',
|
||
tagline: '1899 Original',
|
||
era: 'vintage',
|
||
featureLabel: 'PULL THE LEVER',
|
||
layout: { reels: 3, rows: 1 },
|
||
evaluation: 'lines',
|
||
betLevels: [1, 2, 5, 10, 25],
|
||
symbols: ['bell', 'horseshoe', 'star', 'heart', 'diamond', 'spade'],
|
||
reelStrips: [
|
||
['spade', 'diamond', 'star', 'heart', 'diamond', 'spade', 'horseshoe', 'heart', 'diamond', 'spade',
|
||
'star', 'heart', 'bell', 'diamond', 'spade', 'horseshoe', 'heart', 'diamond', 'star', 'spade'],
|
||
['diamond', 'spade', 'heart', 'star', 'spade', 'diamond', 'heart', 'horseshoe', 'spade', 'diamond',
|
||
'heart', 'star', 'diamond', 'bell', 'spade', 'heart', 'horseshoe', 'diamond', 'spade', 'star'],
|
||
['heart', 'spade', 'diamond', 'star', 'diamond', 'heart', 'spade', 'horseshoe', 'diamond', 'spade',
|
||
'star', 'heart', 'spade', 'diamond', 'bell', 'heart', 'horseshoe', 'spade', 'diamond', 'star'],
|
||
],
|
||
paylines: [[0, 0, 0]],
|
||
paytable: {
|
||
bell: { 3: 150, 2: 12 },
|
||
horseshoe: { 3: 60, 2: 6 },
|
||
star: { 3: 30, 2: 3 },
|
||
heart: { 3: 18, 2: 2 },
|
||
diamond: { 3: 12, 2: 1 },
|
||
spade: { 3: 8, 2: 1 },
|
||
},
|
||
wild: null,
|
||
scatter: null,
|
||
features: { lever: true, steppedStops: true },
|
||
theme: {
|
||
cabinet: 0x5a3a1e, cabinetHi: 0x7a5430, trim: 0xc8a84b, glow: 0xffd98a,
|
||
screenTop: 0x2b1d10, screenBot: 0x191009, accentHex: '#ffd98a', reelBg: 0xf3ead2,
|
||
},
|
||
},
|
||
|
||
// ── 2. Fruit Frenzy — 1960s fruit machine, HOLD & NUDGE ─────────────────────
|
||
{
|
||
id: 'fruit-frenzy',
|
||
name: 'Fruit Frenzy',
|
||
tagline: '1965 Electro-Mechanical',
|
||
era: 'vintage',
|
||
featureLabel: 'HOLD & NUDGE',
|
||
layout: { reels: 3, rows: 3 },
|
||
evaluation: 'lines',
|
||
betLevels: [5, 10, 25, 50],
|
||
symbols: ['bar', 'bell', 'melon', 'plum', 'orange', 'lemon', 'cherry'],
|
||
reelStrips: [
|
||
['cherry', 'lemon', 'plum', 'orange', 'cherry', 'melon', 'lemon', 'plum', 'bell', 'orange',
|
||
'cherry', 'lemon', 'melon', 'plum', 'orange', 'cherry', 'bell', 'lemon', 'bar', 'plum',
|
||
'orange', 'cherry', 'melon', 'bell'],
|
||
['lemon', 'cherry', 'orange', 'plum', 'melon', 'cherry', 'lemon', 'bell', 'plum', 'orange',
|
||
'cherry', 'melon', 'lemon', 'plum', 'bar', 'orange', 'cherry', 'bell', 'lemon', 'plum',
|
||
'cherry', 'orange', 'melon', 'bell'],
|
||
['plum', 'cherry', 'lemon', 'orange', 'cherry', 'melon', 'plum', 'bell', 'lemon', 'orange',
|
||
'cherry', 'plum', 'melon', 'lemon', 'orange', 'bar', 'cherry', 'bell', 'plum', 'lemon',
|
||
'orange', 'cherry', 'melon', 'bell'],
|
||
],
|
||
paylines: LINES_5,
|
||
paytable: {
|
||
bar: { 3: 150 },
|
||
bell: { 3: 80 },
|
||
melon: { 3: 50 },
|
||
plum: { 3: 30 },
|
||
orange: { 3: 20 },
|
||
lemon: { 3: 14 },
|
||
cherry: { 3: 10, 2: 3 },
|
||
},
|
||
wild: null,
|
||
scatter: null,
|
||
features: {
|
||
holdNudge: {
|
||
holdChance: 0.12, // offered after a losing spin (not twice in a row)
|
||
nudgeChance: 0.10, // awarded after a losing spin when holds weren't offered
|
||
nudgeWeights: [[1, 50], [2, 30], [3, 20]],
|
||
},
|
||
},
|
||
theme: {
|
||
cabinet: 0x8c1d2f, cabinetHi: 0xb53247, trim: 0xe8e4da, glow: 0xff7da0,
|
||
screenTop: 0x241016, screenBot: 0x14080c, accentHex: '#ffb3c7', reelBg: 0xfdf6e6,
|
||
},
|
||
},
|
||
|
||
// ── 3. Lucky Sevens — 1970s Vegas, doubling wild diamonds ───────────────────
|
||
{
|
||
id: 'lucky-sevens',
|
||
name: 'Lucky Sevens',
|
||
tagline: '1975 Vegas Strip',
|
||
era: 'vintage',
|
||
featureLabel: 'WILDS DOUBLE PAYS',
|
||
layout: { reels: 3, rows: 1 },
|
||
evaluation: 'lines',
|
||
betLevels: [1, 2, 5, 10, 25],
|
||
symbols: ['wild', 'seven-red', 'seven-white', 'seven-blue', 'bar3', 'bar2', 'bar1'],
|
||
reelStrips: [
|
||
['blank', 'bar1', 'blank', 'seven-white', 'blank', 'bar2', 'blank', 'bar1', 'seven-blue', 'blank',
|
||
'bar3', 'blank', 'bar2', 'blank', 'seven-red', 'blank', 'bar1', 'blank', 'seven-blue', 'blank',
|
||
'bar1', 'blank', 'wild', 'blank'],
|
||
['bar1', 'blank', 'seven-blue', 'blank', 'bar2', 'blank', 'bar1', 'blank', 'seven-white', 'blank',
|
||
'bar3', 'blank', 'bar2', 'blank', 'seven-red', 'blank', 'bar1', 'blank', 'wild', 'blank',
|
||
'bar1', 'blank', 'seven-blue', 'blank'],
|
||
['blank', 'bar2', 'blank', 'seven-white', 'blank', 'bar1', 'blank', 'seven-blue', 'blank', 'bar3',
|
||
'blank', 'bar1', 'blank', 'seven-red', 'blank', 'bar2', 'blank', 'wild', 'blank', 'bar1',
|
||
'blank', 'seven-blue', 'blank', 'bar1'],
|
||
],
|
||
paylines: [[0, 0, 0]],
|
||
paytable: {
|
||
'wild': { 3: 300 },
|
||
'seven-red': { 3: 100 },
|
||
'seven-white': { 3: 80 },
|
||
'seven-blue': { 3: 50 },
|
||
'any-seven': { 3: 12 },
|
||
'bar3': { 3: 25 },
|
||
'bar2': { 3: 15 },
|
||
'bar1': { 3: 10 },
|
||
'any-bar': { 3: 4 },
|
||
},
|
||
symbolGroups: {
|
||
'any-seven': ['seven-red', 'seven-white', 'seven-blue'],
|
||
'any-bar': ['bar1', 'bar2', 'bar3'],
|
||
},
|
||
wild: { symbol: 'wild', multipliers: { 1: 2, 2: 4 } },
|
||
scatter: null,
|
||
features: {},
|
||
theme: {
|
||
cabinet: 0x1b2440, cabinetHi: 0x2c3a66, trim: 0xd4313f, glow: 0x6fa8ff,
|
||
screenTop: 0x10162b, screenBot: 0x080b16, accentHex: '#8fb8ff', reelBg: 0xf2f2f2,
|
||
},
|
||
},
|
||
|
||
// ── 4. Pharaoh's Fortune — free spins + expanding wild ──────────────────────
|
||
{
|
||
id: 'pharaohs-fortune',
|
||
name: "Pharaoh's Fortune",
|
||
tagline: 'Free Spins of the Nile',
|
||
era: 'modern',
|
||
featureLabel: 'FREE SPINS',
|
||
layout: { reels: 5, rows: 3 },
|
||
evaluation: 'lines',
|
||
betLevels: [10, 25, 50, 100, 250],
|
||
symbols: ['pharaoh', 'pyramid', 'sphinx', 'ankh', 'eye', 'cat', 'ace', 'king', 'queen', 'jack'],
|
||
reelStrips: [
|
||
[['sphinx', 3], ['ankh', 4], ['eye', 4], ['cat', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 6], 'pyramid'],
|
||
[['pharaoh', 2], ['sphinx', 3], ['ankh', 4], ['eye', 4], ['cat', 4], ['ace', 5], ['king', 6], ['queen', 6], ['jack', 5], 'pyramid'],
|
||
[['pharaoh', 2], ['sphinx', 3], ['ankh', 3], ['eye', 4], ['cat', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 5], 'pyramid'],
|
||
[['pharaoh', 2], ['sphinx', 3], ['ankh', 4], ['eye', 3], ['cat', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 5], 'pyramid'],
|
||
[['sphinx', 4], ['ankh', 4], ['eye', 4], ['cat', 4], ['ace', 6], ['king', 6], ['queen', 5], ['jack', 6], 'pyramid'],
|
||
],
|
||
paylines: LINES_20,
|
||
paytable: {
|
||
pharaoh: { 3: 85, 4: 340, 5: 1700 },
|
||
sphinx: { 3: 50, 4: 170, 5: 680 },
|
||
ankh: { 3: 40, 4: 135, 5: 500 },
|
||
eye: { 3: 34, 4: 100, 5: 400 },
|
||
cat: { 3: 27, 4: 85, 5: 340 },
|
||
ace: { 3: 17, 4: 50, 5: 200 },
|
||
king: { 3: 14, 4: 40, 5: 170 },
|
||
queen: { 3: 10, 4: 34, 5: 135 },
|
||
jack: { 3: 10, 4: 27, 5: 100 },
|
||
},
|
||
wild: { symbol: 'pharaoh' },
|
||
scatter: {
|
||
symbol: 'pyramid',
|
||
pays: { 3: 2, 4: 10, 5: 50 },
|
||
freeSpins: { 3: 8, 4: 12, 5: 20 },
|
||
retrigger: true,
|
||
expandingWildReel: 2,
|
||
},
|
||
features: {},
|
||
theme: {
|
||
cabinet: 0x223047, cabinetHi: 0x32486b, trim: 0xe6b94d, glow: 0xffd86b,
|
||
screenTop: 0x2c2410, screenBot: 0x120d05, accentHex: '#ffd86b', reelBg: 0x1c1507,
|
||
},
|
||
},
|
||
|
||
// ── 5. Abyssal Treasures — cascading reels + multiplier ladder ──────────────
|
||
{
|
||
id: 'abyssal-treasures',
|
||
name: 'Abyssal Treasures',
|
||
tagline: 'Cascades of the Deep',
|
||
era: 'modern',
|
||
featureLabel: 'CASCADING REELS',
|
||
layout: { reels: 5, rows: 3 },
|
||
evaluation: 'lines',
|
||
betLevels: [10, 25, 50, 100, 250],
|
||
symbols: ['pearl', 'mermaid', 'octopus', 'seahorse', 'angler', 'starfish', 'shell', 'coral', 'bubble'],
|
||
reelStrips: [
|
||
[['mermaid', 3], ['octopus', 4], ['seahorse', 4], ['angler', 4], ['starfish', 6], ['shell', 6], ['coral', 7], ['bubble', 6]],
|
||
[['pearl', 2], ['mermaid', 3], ['octopus', 4], ['seahorse', 4], ['angler', 4], ['starfish', 6], ['shell', 6], ['coral', 6], ['bubble', 5]],
|
||
[['pearl', 2], ['mermaid', 3], ['octopus', 3], ['seahorse', 4], ['angler', 4], ['starfish', 6], ['shell', 6], ['coral', 6], ['bubble', 6]],
|
||
[['pearl', 2], ['mermaid', 3], ['octopus', 4], ['seahorse', 3], ['angler', 4], ['starfish', 6], ['shell', 6], ['coral', 6], ['bubble', 6]],
|
||
[['mermaid', 4], ['octopus', 4], ['seahorse', 4], ['angler', 4], ['starfish', 6], ['shell', 6], ['coral', 6], ['bubble', 6]],
|
||
],
|
||
paylines: LINES_20,
|
||
paytable: {
|
||
pearl: { 3: 50, 4: 200, 5: 750 },
|
||
mermaid: { 3: 30, 4: 100, 5: 375 },
|
||
octopus: { 3: 22, 4: 75, 5: 250 },
|
||
seahorse: { 3: 18, 4: 50, 5: 200 },
|
||
angler: { 3: 15, 4: 38, 5: 150 },
|
||
starfish: { 3: 10, 4: 25, 5: 100 },
|
||
shell: { 3: 8, 4: 20, 5: 75 },
|
||
coral: { 3: 6, 4: 15, 5: 62 },
|
||
bubble: { 3: 6, 4: 12, 5: 50 },
|
||
},
|
||
wild: { symbol: 'pearl' },
|
||
scatter: null,
|
||
features: { cascade: { multipliers: [1, 2, 4, 6] } },
|
||
theme: {
|
||
cabinet: 0x0d2f3d, cabinetHi: 0x15485e, trim: 0x3fd6c0, glow: 0x59e7ff,
|
||
screenTop: 0x06222e, screenBot: 0x020d13, accentHex: '#59e7ff', reelBg: 0x04161f,
|
||
},
|
||
},
|
||
|
||
// ── 6. Dragon's Hoard — hold-and-spin coins + progressive jackpot ───────────
|
||
{
|
||
id: 'dragons-hoard',
|
||
name: "Dragon's Hoard",
|
||
tagline: 'Hold & Spin Jackpots',
|
||
era: 'modern',
|
||
featureLabel: 'JACKPOT COINS',
|
||
layout: { reels: 5, rows: 3 },
|
||
evaluation: 'lines',
|
||
betLevels: [10, 25, 50, 100, 250],
|
||
symbols: ['dragon', 'coin', 'knight', 'castle', 'chest', 'gem', 'ace', 'king', 'queen', 'jack'],
|
||
reelStrips: [
|
||
[['coin', 4, 2], ['knight', 3], ['castle', 3], ['chest', 3], ['gem', 4], ['ace', 5], ['king', 5], ['queen', 6], ['jack', 7]],
|
||
[['dragon', 2], ['coin', 4, 2], ['knight', 3], ['castle', 3], ['chest', 3], ['gem', 4], ['ace', 5], ['king', 5], ['queen', 5], ['jack', 6]],
|
||
[['dragon', 2], ['coin', 4, 2], ['knight', 3], ['castle', 3], ['chest', 3], ['gem', 4], ['ace', 5], ['king', 5], ['queen', 5], ['jack', 6]],
|
||
[['dragon', 2], ['coin', 4, 2], ['knight', 3], ['castle', 3], ['chest', 3], ['gem', 4], ['ace', 5], ['king', 5], ['queen', 5], ['jack', 6]],
|
||
[['coin', 4, 2], ['knight', 3], ['castle', 4], ['chest', 4], ['gem', 4], ['ace', 5], ['king', 5], ['queen', 5], ['jack', 6]],
|
||
],
|
||
paylines: LINES_20,
|
||
paytable: {
|
||
dragon: { 3: 60, 4: 240, 5: 1200 },
|
||
knight: { 3: 36, 4: 120, 5: 450 },
|
||
castle: { 3: 30, 4: 90, 5: 360 },
|
||
chest: { 3: 24, 4: 75, 5: 300 },
|
||
gem: { 3: 18, 4: 60, 5: 240 },
|
||
ace: { 3: 12, 4: 36, 5: 150 },
|
||
king: { 3: 12, 4: 30, 5: 120 },
|
||
queen: { 3: 9, 4: 24, 5: 90 },
|
||
jack: { 3: 6, 4: 18, 5: 75 },
|
||
},
|
||
wild: { symbol: 'dragon' },
|
||
scatter: null,
|
||
features: {
|
||
holdSpin: {
|
||
trigger: 6,
|
||
respins: 3,
|
||
coinSymbol: 'coin',
|
||
coinChance: 0.085, // per empty cell, per respin
|
||
// [value-or-tier, weight]; numeric values are ×total-bet
|
||
coinValues: [[1, 40], [2, 28], [3, 15], [5, 10], [10, 5], ['mini', 1.5], ['minor', 0.4], ['major', 0.1]],
|
||
jackpots: { mini: 20, minor: 60, major: 250 }, // ×total-bet
|
||
grand: { seed: 5000, sliver: 0.01 }, // progressive, localStorage-backed
|
||
},
|
||
},
|
||
theme: {
|
||
cabinet: 0x33121c, cabinetHi: 0x55202e, trim: 0xe8483f, glow: 0xff8c3b,
|
||
screenTop: 0x2a0f08, screenBot: 0x120503, accentHex: '#ff9d4d', reelBg: 0x190a06,
|
||
},
|
||
},
|
||
|
||
// ── 7. Gold Rush Gulch — sticky wilds + pick-a-cart bonus ───────────────────
|
||
{
|
||
id: 'gold-rush',
|
||
name: 'Gold Rush Gulch',
|
||
tagline: 'Sticky Bandits & Mine Carts',
|
||
era: 'modern',
|
||
featureLabel: 'PICK BONUS',
|
||
layout: { reels: 5, rows: 3 },
|
||
evaluation: 'lines',
|
||
betLevels: [10, 25, 50, 100, 250],
|
||
symbols: ['bandit', 'dynamite', 'sheriff', 'saloon', 'wagon', 'pickaxe', 'ace', 'king', 'queen', 'jack'],
|
||
reelStrips: [
|
||
[['sheriff', 3], ['saloon', 3], ['wagon', 4], ['pickaxe', 4], ['ace', 6], ['king', 6], ['queen', 7], ['jack', 6], 'dynamite'],
|
||
['bandit', ['sheriff', 3], ['saloon', 3], ['wagon', 4], ['pickaxe', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 6], 'dynamite'],
|
||
['bandit', ['sheriff', 3], ['saloon', 3], ['wagon', 4], ['pickaxe', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 6], 'dynamite'],
|
||
['bandit', ['sheriff', 3], ['saloon', 3], ['wagon', 4], ['pickaxe', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 6], 'dynamite'],
|
||
[['sheriff', 3], ['saloon', 4], ['wagon', 4], ['pickaxe', 4], ['ace', 6], ['king', 6], ['queen', 6], ['jack', 6], 'dynamite'],
|
||
],
|
||
paylines: LINES_20,
|
||
paytable: {
|
||
sheriff: { 3: 30, 4: 100, 5: 380 },
|
||
saloon: { 3: 25, 4: 75, 5: 300 },
|
||
wagon: { 3: 20, 4: 64, 5: 255 },
|
||
pickaxe: { 3: 15, 4: 46, 5: 180 },
|
||
ace: { 3: 10, 4: 30, 5: 128 },
|
||
king: { 3: 10, 4: 26, 5: 100 },
|
||
queen: { 3: 8, 4: 20, 5: 75 },
|
||
jack: { 3: 5, 4: 15, 5: 64 },
|
||
},
|
||
wild: { symbol: 'bandit' },
|
||
scatter: { symbol: 'dynamite', pays: { 3: 2, 4: 8, 5: 30 } },
|
||
features: {
|
||
stickyWilds: { symbol: 'bandit', spins: 3 },
|
||
pickBonus: {
|
||
scatter: 'dynamite', min: 3, picks: 3, carts: 9,
|
||
prizes: [[2, 30], [3, 25], [5, 20], [8, 12], [12, 7], [20, 4], [50, 2]], // ×total-bet
|
||
},
|
||
},
|
||
theme: {
|
||
cabinet: 0x4a3217, cabinetHi: 0x6e4c24, trim: 0xf0c441, glow: 0xffe27a,
|
||
screenTop: 0x271c0c, screenBot: 0x110c05, accentHex: '#ffe27a', reelBg: 0x1c1409,
|
||
},
|
||
},
|
||
|
||
// ── 8. Sugar Spin — 6×5 scatter-pays tumble + rainbow multipliers ───────────
|
||
{
|
||
id: 'sugar-spin',
|
||
name: 'Sugar Spin',
|
||
tagline: 'Tumbling Candy Mountain',
|
||
era: 'modern',
|
||
featureLabel: 'TUMBLE WINS',
|
||
layout: { reels: 6, rows: 5 },
|
||
evaluation: 'scatterPays',
|
||
betLevels: [10, 25, 50, 100, 250],
|
||
symbols: ['rainbow', 'chocolate', 'candy-cane', 'gummy', 'lollipop', 'jelly', 'mint', 'berry', 'sugar'],
|
||
reelStrips: [
|
||
[['chocolate', 2, 2], ['candy-cane', 2, 2], ['gummy', 3, 2], ['lollipop', 4, 2], ['jelly', 4, 2], ['mint', 5, 2], ['berry', 5, 2], ['sugar', 5, 2]],
|
||
[['chocolate', 2, 2], ['candy-cane', 2, 2], ['gummy', 3, 2], ['lollipop', 4, 2], ['jelly', 4, 2], ['mint', 5, 2], ['berry', 5, 2], ['sugar', 5, 2]],
|
||
[['chocolate', 2, 2], ['candy-cane', 2, 2], ['gummy', 3, 2], ['lollipop', 4, 2], ['jelly', 4, 2], ['mint', 5, 2], ['berry', 5, 2], ['sugar', 5, 2]],
|
||
[['chocolate', 2, 2], ['candy-cane', 2, 2], ['gummy', 3, 2], ['lollipop', 4, 2], ['jelly', 4, 2], ['mint', 5, 2], ['berry', 5, 2], ['sugar', 5, 2]],
|
||
[['chocolate', 2, 2], ['candy-cane', 2, 2], ['gummy', 3, 2], ['lollipop', 4, 2], ['jelly', 4, 2], ['mint', 5, 2], ['berry', 5, 2], ['sugar', 5, 2]],
|
||
[['chocolate', 2, 2], ['candy-cane', 2, 2], ['gummy', 3, 2], ['lollipop', 4, 2], ['jelly', 4, 2], ['mint', 5, 2], ['berry', 5, 2], ['sugar', 5, 2]],
|
||
],
|
||
paylines: null,
|
||
// scatterPays tiers: count → ×total-bet, highest qualifying tier wins.
|
||
paytable: {
|
||
'chocolate': [{ min: 12, pay: 16 }, { min: 10, pay: 8 }, { min: 8, pay: 3 }],
|
||
'candy-cane': [{ min: 12, pay: 8 }, { min: 10, pay: 4 }, { min: 8, pay: 1.6 }],
|
||
'gummy': [{ min: 12, pay: 5 }, { min: 10, pay: 2.5 }, { min: 8, pay: 1 }],
|
||
'lollipop': [{ min: 12, pay: 4 }, { min: 10, pay: 1.6 }, { min: 8, pay: 0.6 }],
|
||
'jelly': [{ min: 12, pay: 3 }, { min: 10, pay: 1.2 }, { min: 8, pay: 0.5 }],
|
||
'mint': [{ min: 12, pay: 2.5 }, { min: 10, pay: 0.8 }, { min: 8, pay: 0.3 }],
|
||
'berry': [{ min: 12, pay: 2 }, { min: 10, pay: 0.6 }, { min: 8, pay: 0.25 }],
|
||
'sugar': [{ min: 12, pay: 1.5 }, { min: 10, pay: 0.5 }, { min: 8, pay: 0.15 }],
|
||
},
|
||
wild: null,
|
||
scatter: null,
|
||
features: {
|
||
scatterPays: { min: 8 },
|
||
tumble: true,
|
||
rainbow: {
|
||
symbol: 'rainbow',
|
||
chance: 0.015, // per refilled cell during tumbles
|
||
values: [[2, 55], [3, 25], [5, 15], [10, 5]],
|
||
},
|
||
},
|
||
theme: {
|
||
cabinet: 0x6e2160, cabinetHi: 0x933582, trim: 0xff8ad2, glow: 0xffa3e0,
|
||
screenTop: 0x35103a, screenBot: 0x16071d, accentHex: '#ffa3e0', reelBg: 0x230b2a,
|
||
},
|
||
},
|
||
];
|
||
|
||
export const MACHINE_BY_ID = Object.fromEntries(MACHINES.map((m) => [m.id, m]));
|
||
|
||
// Resolve strips once; consumers should use machine.strips (expanded).
|
||
// Hand-authored strips (all single entries) keep their written order;
|
||
// compressed strips are interleaved so identical symbols never clump.
|
||
for (const m of MACHINES) {
|
||
m.strips = m.reelStrips.map((s) => (s.some(Array.isArray) ? interleaveStrip(s) : expandStrip(s)));
|
||
}
|