slot-machine-gen
Version:
Create an extremely biased, web-based slot machine game.
349 lines (326 loc) • 8.73 kB
JavaScript
/**
* Slot Machine Generator
* Create an extremely biased, web-based slot machine game.
*
* Copyright 2020-2025, Marc S. Brooks (https://mbrooks.info)
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*/
;
/**
* @param {Element} container
* Containing HTML element.
*
* @param {Array<Object>} reels
* Reel configuration.
*
* @param {Function} callback
* Returns selected pay-line symbols.
*
* @param {Object} options
* Configuration overrides (optional).
*/
function SlotMachine(container, reels, callback, options) {
var self = this;
var REEL_SEGMENT_TOTAL = 24;
var defaults = {
reelHeight: 1200,
reelWidth: 200,
reelOffset: 20,
slotYAxis: 0,
animSpeed: 1000,
click2Spin: true,
sounds: {
reelsBegin: null,
reelsEnd: null
},
rngFunc: function rngFunc() {
// The weakest link.
return Math.random();
}
};
(function () {
self.options = Object.assign(defaults, options);
if (reels.length > 0) {
initGame();
} else {
throw new Error('Failed to initialize (missing reels)');
}
})();
/**
* Initialize a new game instance.
*/
function initGame() {
container.setAttribute('aria-label', 'Slot machine');
createDisplayElm();
createSlotElm();
}
/**
* Create display elements.
*/
function createDisplayElm() {
var div = document.createElement('div');
div.classList.add('display');
for (var i = 0; i < reels.length; i++) {
var elm = document.createElement('div');
elm.classList.add('reel');
elm.setAttribute('role', 'none');
elm.style.transform = "rotateY(" + self.options.slotYAxis + "deg)";
div.appendChild(elm);
}
if (self.options.click2Spin) {
var title = 'Click to spin';
// Add event to display to spin reels.
div.addEventListener('click', spinReels);
div.setAttribute('aria-label', title);
div.setAttribute('role', 'button');
div.setAttribute('title', title);
div.style.cursor = 'pointer';
}
container.appendChild(div);
}
/**
* Create slot elements.
*/
function createSlotElm() {
var div = document.createElement('div');
div.classList.add('slots');
div.setAttribute('aria-label', 'Reels');
reels.forEach(function (reel, index) {
var elm = createReelElm(reel, reel.symbols[0].position);
elm.setAttribute('aria-label', "Reel " + (index + 1));
div.appendChild(elm);
});
container.appendChild(div);
}
/**
* Create reel elements.
*
* @param {Object} config
* Config options.
*
* @param {Number} startPos
* Start position.
*
* @return {Element}
*/
function createReelElm(config, startPos) {
if (startPos === void 0) {
startPos = 0;
}
var div = document.createElement('div');
div.style.transform = "rotateY(" + self.options.slotYAxis + "deg)";
div.classList.add('reel');
var elm = createStripElm(config, config.symbols[0].position);
config['element'] = elm;
div.appendChild(elm);
return div;
}
/**
* Create strip elements (faux-panoramic animation).
*
* @param {Object} config
* Config options.
*
* @param {Number} startPos
* Start position.
*
* @return {Element}
*/
function createStripElm(config, startPos) {
if (startPos === void 0) {
startPos = 0;
}
var stripHeight = getStripHeight();
var stripWidth = getStripWidth();
var segmentDeg = 360 / REEL_SEGMENT_TOTAL;
var transZ = Math.trunc(Math.tan(90 / Math.PI - segmentDeg) * (stripHeight * 0.5) * 4);
var marginTop = transZ + stripHeight / 2;
var ul = document.createElement('ul');
ul.style.height = stripHeight + 'px';
ul.style.marginTop = marginTop + 'px';
ul.style.width = stripWidth + 'px';
ul.classList.add('strip');
for (var i = 0; i < REEL_SEGMENT_TOTAL; i++) {
var li = document.createElement('li');
li.append(i.toString());
var imgPosY = getImagePosY(i, startPos);
var rotateX = REEL_SEGMENT_TOTAL * segmentDeg - i * segmentDeg;
// Position image per the strip angle/container radius.
li.style.background = "url(" + config.imageSrc + ") 0 " + imgPosY + "px";
li.style.height = stripHeight + 'px';
li.style.width = stripWidth + 'px';
li.style.transform = "rotateX(" + rotateX + "deg) translateZ(" + transZ + "px)";
ul.appendChild(li);
}
return ul;
}
/**
* Select a random symbol by weight.
*
* @param {Array<Object>} symbols
* List of symbols.
*
* @return {Object}
*/
function selectRandSymbol(symbols) {
var totalWeight = 0;
var symbolTotal = symbols.length;
for (var i = 0; i < symbolTotal; i++) {
var symbol = symbols[i];
var weight = symbol.weight;
totalWeight += weight;
}
var randNum = getRandom() * totalWeight;
for (var j = 0; j < symbolTotal; j++) {
var _symbol = symbols[j];
var _weight = _symbol.weight;
if (randNum < _weight) {
return _symbol;
}
randNum -= _weight;
}
}
/**
* Spin the reels and try your luck.
*/
function spinReels() {
var payLine = [];
if (callback) {
// Delay callback until animations have stopped.
payLine.push = function () {
Array.prototype.push.apply(this, arguments);
if (payLine.length === reels.length) {
window.setTimeout(function () {
self.isAnimating = false;
callback(payLine);
}, self.options.animSpeed);
}
};
}
playSound(self.options.sounds.reelsBegin);
reels.forEach(function (reel) {
var selected = selectRandSymbol(reel.symbols);
var startPos = selected.position;
// Start the rotation animation.
var elm = reel.element;
elm.classList.remove('stop');
elm.classList.toggle('spin');
// Shift images to select position.
elm.childNodes.forEach(function (li, index) {
li.style.backgroundPositionY = getImagePosY(index, startPos) + 'px';
});
// Randomly stop rotation animation.
window.setTimeout(function () {
elm.classList.replace('spin', 'stop');
playSound(self.options.sounds.reelsEnd);
payLine.push(selected);
}, self.options.animSpeed * getRandomInt(1, 4));
});
}
/**
* Get random number between 0 (inclusive) and 1 (exclusive).
*
* @return {number}
*/
function getRandom() {
return self.options.rngFunc();
}
/**
* Get random integer between two values.
*
* @param {Number} min
* Minimum value (default: 0).
*
* @param {Number} max
* Maximum value (default: 10).
*
* @return {Number}
*/
function getRandomInt(min, max) {
if (min === void 0) {
min = 1;
}
if (max === void 0) {
max = 10;
}
var minNum = Math.ceil(min);
var maxNum = Math.floor(max);
return Math.floor(getRandom() * (Math.floor(maxNum) - minNum)) + minNum;
}
/**
* Calculate the strip background position.
*
* @param {Number} index
* Strip symbol index.
*
* @param {Number} position
* Strip target position.
*
* @return {Number}
*/
function getImagePosY(index, position) {
return -Math.abs(getStripHeight() * index + (position - self.options.reelOffset));
}
/**
* Calculate the strip height.
*
* @return {Number}
*/
function getStripHeight() {
return self.options.reelHeight / REEL_SEGMENT_TOTAL;
}
/**
* Calculate the strip width.
*
* @return {Number}
*/
function getStripWidth() {
return self.options.reelWidth;
}
/**
* Play the audio clip.
*
* @param {String} url
* Audio file URL.
*/
function playSound(url) {
if (url) {
var audio = new Audio();
audio.src = url;
audio.onerror = function () {
return console.warn("Failed to load audio: " + url);
};
audio.play();
}
}
/**
* Dispatch game actions.
*
* @param {Function} func
* Function to execute.
*/
function dispatch(func) {
if (!self.isAnimating) {
self.isAnimating = true;
func.call(self);
}
}
/**
* Protected members.
*/
self.play = function () {
dispatch(spinReels);
};
return self;
}
/**
* Set global/exportable instance, where supported.
*/
window.slotMachine = function (container, reels, callback, options) {
return new SlotMachine(container, reels, callback, options);
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = SlotMachine;
}
//# sourceMappingURL=slot-machine.js.map