fertig-classic-games/src/games/slots/machines.js

434 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)));
}