├── .gitignore ├── package.gif ├── demo ├── sounds │ ├── winner.mp3 │ ├── reelsEnd.mp3 │ └── reelsBegin.mp3 ├── css │ └── demo.css └── index.html ├── images ├── reel-strip.psd ├── reel-strip1.png ├── reel-strip2.png └── reel-strip3.png ├── .vscode ├── settings.json └── extensions.json ├── .npmignore ├── babel.config.json ├── SECURITY.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── eslint.config.js ├── CHANGELOG.md ├── package.json ├── src ├── slot-machine.scss └── slot-machine.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /package.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/package.gif -------------------------------------------------------------------------------- /demo/sounds/winner.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/demo/sounds/winner.mp3 -------------------------------------------------------------------------------- /images/reel-strip.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/images/reel-strip.psd -------------------------------------------------------------------------------- /images/reel-strip1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/images/reel-strip1.png -------------------------------------------------------------------------------- /images/reel-strip2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/images/reel-strip2.png -------------------------------------------------------------------------------- /images/reel-strip3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/images/reel-strip3.png -------------------------------------------------------------------------------- /demo/sounds/reelsEnd.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/demo/sounds/reelsEnd.mp3 -------------------------------------------------------------------------------- /demo/sounds/reelsBegin.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxy/slot-machine-gen/HEAD/demo/sounds/reelsBegin.mp3 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD014": false, 4 | "MD045": false, 5 | "MD046": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | babel.config.json 3 | eslint.config.js 4 | ISSUE_TEMPLATE.md 5 | package.* 6 | demo 7 | images/*.psd 8 | node_modules 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "christian-kohler.path-intellisense", 4 | "DavidAnson.vscode-markdownlint", 5 | "dbaeumer.vscode-eslint", 6 | "donjayamanne.githistory", 7 | "liamhammett.inline-parameters", 8 | "steoates.autoimport" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "loose": true, 7 | "exclude": [ 8 | "@babel/plugin-proposal-dynamic-import" 9 | ] 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | [ 15 | "@babel/plugin-proposal-decorators", 16 | { 17 | "legacy": true 18 | } 19 | ], 20 | [ 21 | "@babel/plugin-proposal-class-properties", 22 | { 23 | "loose": true 24 | } 25 | ], 26 | "@babel/plugin-syntax-dynamic-import" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported releases 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.1.x | :x: | 8 | | 1.2.x | :white_check_mark: | 9 | | 1.3.x | :white_check_mark: | 10 | 11 | ## Reporting an issue 12 | 13 | If you find a vulnerability with this package, please report it [here](https://github.com/nuxy/slot-machine-gen/issues) for full disclosure. 14 | 15 | ## Contributions 16 | 17 | If you fix a bug, or have a code you want to contribute, please send a pull-request with your changes. (Note: Before committing your code please ensure that you are following the [Node.js style guide](https://github.com/felixge/node-style-guide)) 18 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #24292f; 3 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif; 4 | font-size: 14px; 5 | } 6 | 7 | #slot-machine { 8 | margin: 10% auto 0 auto; 9 | } 10 | 11 | #slot-credits { 12 | text-align: center; 13 | margin: 20px; 14 | } 15 | 16 | #play-button { 17 | display: block; 18 | margin: 0 auto; 19 | width: 100px; 20 | } 21 | 22 | /** 23 | * Responsive overrides. 24 | */ 25 | @media only screen and (min-device-width: 0px) and (max-device-width: 450px) { 26 | html { 27 | min-width: 400px; 28 | } 29 | 30 | #slot-machine { 31 | margin: 0 auto -150px auto; 32 | transform: scale(65%); 33 | transform-origin: left top; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue template 2 | 3 | 6 | 7 | I'm submitting a.. 8 | 9 | - [ ] Bug report 10 | - [ ] Feature request 11 | 12 | ## Environment info 13 | 14 | ### Operating System 15 | 16 | - [ ] Linux 17 | - [ ] OSX 10.x 18 | - [ ] Windows 10 19 | 20 | 23 | 24 | ### Node version 25 | 26 | 16.8.0 27 | 28 | 32 | 33 | ### NPM version 34 | 35 | 8.19.4 36 | 37 | 41 | 42 | ### Web browser 43 | 44 | - [ ] Chrome 45 | - [ ] Firefox 46 | - [ ] Edge 47 | - [ ] Safari 48 | 49 | #### Version 50 | 51 | latest 52 | 53 | ## Current behavior 54 | 55 | 58 | 59 | ### Bug report 60 | 61 | 64 | 65 | ### Feature request 66 | 67 | 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Marc S. Brooks (https://mbrooks.info) 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const {defineConfig} = require('eslint/config'); 2 | const babelParser = require('@babel/eslint-parser'); 3 | 4 | module.exports = defineConfig([ 5 | { 6 | languageOptions: { 7 | globals: { 8 | es6: true, 9 | node: true 10 | }, 11 | parser: babelParser, 12 | parserOptions: { 13 | ecmaVersion: 2021, 14 | sourceType: 'module' 15 | } 16 | }, 17 | rules: { 18 | 'array-bracket-spacing': [ 19 | 2, 20 | 'never' 21 | ], 22 | 'block-scoped-var': 2, 23 | 'brace-style': [ 24 | 2, 25 | '1tbs' 26 | ], 27 | 'camelcase': 1, 28 | 'computed-property-spacing': [ 29 | 2, 30 | 'never' 31 | ], 32 | 'curly': 2, 33 | 'eol-last': 2, 34 | 'eqeqeq': [ 35 | 2, 36 | 'smart' 37 | ], 38 | 'max-depth': [ 39 | 1, 40 | 3 41 | ], 42 | 'new-cap': 0, 43 | 'no-extend-native': 2, 44 | 'no-mixed-spaces-and-tabs': 2, 45 | 'no-trailing-spaces': 2, 46 | 'no-unused-vars': 0, 47 | 'no-use-before-define': [ 48 | 2, 49 | 'nofunc' 50 | ], 51 | 'object-curly-spacing': [ 52 | 2, 53 | 'never' 54 | ], 55 | 'quotes': [ 56 | 2, 57 | 'single', 58 | 'avoid-escape' 59 | ], 60 | 'semi': [ 61 | 2, 62 | 'always' 63 | ], 64 | 'keyword-spacing': [ 65 | 2, 66 | { 67 | 'before': true, 68 | 'after': true 69 | } 70 | ], 71 | 'space-unary-ops': 2 72 | } 73 | } 74 | ]); 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.3.0] - 2023-10-12 8 | 9 | ### Changed 10 | 11 | - Refactor timers, removed clearTimeout 12 | - Package dependency updates 13 | 14 | ## [1.3.1] - 2023-11-19 15 | 16 | ### Updated 17 | 18 | - Package keywords / documented features 19 | - Removed commented artifacts 20 | 21 | ## [1.3.2] - 2023-12-03 22 | 23 | ### Added 24 | 25 | - Slot machine credit/payouts logic to demo. 26 | 27 | ### Removed 28 | 29 | - Photoshop `reel-strip.psd` from NPM package. 30 | 31 | ## [1.3.3] - 2024-03-14 32 | 33 | ### Added 34 | 35 | - Support WCAG 2.1 aria roles 36 | - Added `click2Spin` event option 37 | 38 | ### Changed 39 | 40 | - Replace this referenced to self 41 | - Upgraded outdated NPM dependencies 42 | 43 | ## [1.3.4] - 2024-04-01 44 | 45 | - Update [height -> inherit from parent](https://github.com/nuxy/slot-machine-gen/commit/8ee0ef24717d79b8db7a1277c884451cd1597199) 46 | 47 | ## [1.3.5] - 2024-07-13 48 | 49 | - NPM security updates ([braces](https://github.com/advisories/GHSA-grv7-fg5c-xmjg) override) 50 | 51 | ## [1.3.6] - 2025-03-08 52 | 53 | - NPM security updates ([cross-spawn](https://github.com/advisories/GHSA-3xgq-45jj-v275)/[micromatch](https://github.com/advisories/GHSA-952p-6rrq-rcjv)) 54 | - Updated demo - Added device only responsice overrides 55 | 56 | ## 1.3.7 - 2025-10-17 57 | 58 | - Upgraded outdated NPM dependencies / NPM security updates 59 | - Replaced ESLint deprecated release 60 | - Renamed Babel config to recommended 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slot-machine-gen", 3 | "version": "1.3.7", 4 | "description": "Create an extremely biased, web-based slot machine game.", 5 | "main": "src/slot-machine.js", 6 | "scripts": { 7 | "build": "babel src -s -D -d dist && npm run sass && npm run minify-css && npm run minify-js", 8 | "lint": "eslint src", 9 | "sass": "sass src/slot-machine.scss dist/slot-machine.css", 10 | "minify-css": "node-minify --compressor clean-css --input 'dist/slot-machine.css' --output 'dist/slot-machine.min.css'", 11 | "minify-js": "node-minify --compressor uglify-js --input 'dist/slot-machine.js' --output 'dist/slot-machine.min.js'", 12 | "prepack": "npm run build", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/nuxy/slot-machine-gen.git" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "html", 22 | "web-browser", 23 | "plugin", 24 | "animation", 25 | "slot-machine", 26 | "game", 27 | "3d-cylinder" 28 | ], 29 | "bugs": { 30 | "url": "https://github.com/nuxy/slot-machine-gen/issues" 31 | }, 32 | "homepage": "https://github.com/nuxy/slot-machine-gen#readme", 33 | "author": "Marc S. Brooks (https://mbrooks.info)", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@babel/cli": "^7.28.3", 37 | "@babel/core": "^7.28.4", 38 | "@babel/eslint-parser": "^7.28.4", 39 | "@babel/plugin-proposal-class-properties": "^7.17.12", 40 | "@babel/plugin-proposal-decorators": "^7.28.0", 41 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 42 | "@babel/preset-env": "^7.28.3", 43 | "@babel/register": "^7.28.3", 44 | "@node-minify/clean-css": "^8.0.6", 45 | "@node-minify/cli": "^8.0.6", 46 | "@node-minify/uglify-js": "^8.0.6", 47 | "eslint": "^9.37.0", 48 | "sass": "^1.93.2" 49 | }, 50 | "overrides": { 51 | "braces": "3.0.3", 52 | "got": "^12.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/slot-machine.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Slot Machine Generator 3 | * Create an extremely biased, web-based slot machine game. 4 | * 5 | * Copyright 2020-202s54, Marc S. Brooks (https://mbrooks.info) 6 | * Licensed under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | .slot-machine { 11 | height: 480px; 12 | width: 600px; 13 | 14 | .display { 15 | display: flex; 16 | height: inherit; 17 | position: absolute; 18 | transform: translateZ(0); 19 | width: inherit; 20 | z-index: 2; 21 | 22 | .reel { 23 | background: transparent; 24 | border: 1px solid #999; 25 | border-radius: 10px; 26 | height: calc(100% - 103px); 27 | margin: 47px 10px; 28 | overflow: hidden; 29 | width: 200px; 30 | 31 | &:after { 32 | box-shadow: inset -3em -3em 4em #0C090A; 33 | } 34 | 35 | &:before { 36 | box-shadow: inset -3em 3em 4em #0C090A; 37 | } 38 | 39 | &:after, 40 | &:before { 41 | content: ''; 42 | display: block; 43 | height: 50%; 44 | width: calc(100% + 100px); 45 | } 46 | } 47 | } 48 | 49 | .slots { 50 | height: inherit; 51 | z-index: 1; 52 | --webkit-transform: translate3d(0, 0, 0); 53 | 54 | .reel { 55 | display: inline-block; 56 | height: inherit; 57 | 58 | .strip { 59 | list-style: none; 60 | margin-left: -40px; 61 | transform-style: preserve-3d; 62 | 63 | &.start { 64 | animation: init 300ms ease-in reverse; 65 | } 66 | 67 | &.spin { 68 | animation: spin 600ms linear infinite reverse; 69 | } 70 | 71 | &.stop { 72 | animation: init 1000ms ease-out reverse 73 | } 74 | 75 | li { 76 | font-size: 0; 77 | position: absolute; 78 | text-align: center; 79 | -webkit-backface-visibility: hidden; 80 | } 81 | } 82 | } 83 | } 84 | 85 | @-moz-keyframes init { 86 | from { 87 | -moz-transform: rotateX(0); 88 | } 89 | to { 90 | -moz-transform: rotateX(15deg); 91 | } 92 | } 93 | 94 | @-webkit-keyframes init { 95 | from { 96 | -webkit-transform: rotateX(0); 97 | } 98 | to { 99 | -webkit-transform: rotateX(15deg); 100 | } 101 | } 102 | 103 | @-moz-keyframes spin { 104 | from { 105 | -moz-transform: rotateX(0); 106 | } 107 | to { 108 | -moz-transform: rotateX(360deg); 109 | } 110 | } 111 | 112 | @-webkit-keyframes spin { 113 | from { 114 | -webkit-transform: rotateX(0); 115 | } 116 | to { 117 | -webkit-transform: rotateX(360deg); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 184 | 185 | 186 | 187 | 192 | 193 | Create an extremely biased, web-based slot machine game | Slot Machine Generator 194 | 195 | 196 | 197 | 198 | 203 | 204 | 205 |
206 |
207 | Credits 5 208 |
209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slot Machine Generator 2 | 3 | [![npm version](https://badge.fury.io/js/slot-machine-gen.svg)](https://badge.fury.io/js/slot-machine-gen) [![](https://img.shields.io/npm/dm/slot-machine-gen)](https://www.npmjs.com/package/slot-machine-gen) [![Install size](https://packagephobia.com/badge?p=slot-machine-gen)](https://packagephobia.com/result?p=slot-machine-gen) [![](https://img.shields.io/github/v/release/nuxy/slot-machine-gen)](https://github.com/nuxy/slot-machine-gen/releases) [![NO AI](https://raw.githubusercontent.com/nuxy/no-ai-badge/master/badge.svg)](https://github.com/nuxy/no-ai-badge) 4 | 5 | Create an extremely biased, web-based slot machine game. 6 | 7 | ![Preview](https://raw.githubusercontent.com/nuxy/slot-machine-gen/master/package.gif) 8 | 9 | ## Features 10 | 11 | - Faux-panoramic reel animations (**3D cylinder**, without ``) 12 | - Support for single/multi-line reels and pay-lines. 13 | - Pseudo-random selections by configured weight. 14 | - Configurable RNG (to make it less biased) 15 | - Configurable sound clips for reel animations. 16 | - Easy to set-up and customize. **No dependencies**. 17 | 18 | Checkout the [demo](https://nuxy.github.io/slot-machine-gen) for examples of use. 19 | 20 | ## Dependencies 21 | 22 | - [Node.js](https://nodejs.org) 23 | 24 | ## Installation 25 | 26 | Install the package into your project using [NPM](https://npmjs.com), or download the [sources](https://github.com/nuxy/slot-machine-gen/archive/master.zip). 27 | 28 | $ npm install slot-machine-gen 29 | 30 | ### Alternative 31 | 32 | To add to an existing [React](https://reactjs.org) or [Vue](https://vuejs.org) project you can install this package using [YARN](https://yarnpkg.com). 33 | 34 | #### React 35 | 36 | $ yarn add react-slot-machine-gen 37 | 38 | #### Vue 39 | 40 | $ yarn add vue-slot-machine-gen 41 | 42 | ## Usage 43 | 44 | There are two ways you can use this package. One is by including the JavaScript/CSS sources directly. The other is by importing the module into your component. 45 | 46 | ### Script include 47 | 48 | After you [build the distribution sources](#cli-options) the set-up is fairly simple.. 49 | 50 | ```html 51 | 52 | 53 | 54 | 57 | ``` 58 | 59 | ### Module import 60 | 61 | If your using a modern framework like [Aurelia](https://aurelia.io), [Angular](https://angular.io), [React](https://reactjs.org), or [Vue](https://vuejs.org) 62 | 63 | ```javascript 64 | import SlotMachine from 'slot-machine-gen'; 65 | import 'slot-machine-gen/dist/slot-machine.css'; 66 | 67 | const slotMachine = new SlotMachine(container, reels, callback, options); 68 | ``` 69 | 70 | ### HTML markup 71 | 72 | ```html 73 |
74 | ``` 75 | 76 | ## Reels configuration 77 | 78 | Outside of a reel image source, `symbols` must contain the following: 79 | 80 | | Key | Description | Type | 81 | |----------|---------------------------------------------------------|--------| 82 | | title | Name of the strip symbol | String | 83 | | position | Symbol center (in pixels) calculated from the strip top | Number | 84 | | weight | Selection weight (>1 increases odds) | Number | 85 | 86 | ### Example 87 | 88 | ```javascript 89 | const reels = [ 90 | { 91 | imageSrc: 'path/to/image.png', 92 | symbols: [ 93 | { 94 | title: 'cherry', 95 | position: 100, 96 | weight: 2 97 | }, 98 | { 99 | title: 'plum', 100 | position: 300, 101 | weight: 6 102 | }, 103 | { 104 | title: 'orange', 105 | position: 500, 106 | weight: 5 107 | }, 108 | { 109 | title: 'bell', 110 | position: 700, 111 | weight: 1 112 | }, 113 | { 114 | title: 'cherry', 115 | position: 900, 116 | weight: 3 117 | }, 118 | { 119 | title: 'plum', 120 | position: 1100, 121 | weight: 5 122 | } 123 | } 124 | }, 125 | 126 | // add more reels ... 127 | ] 128 | ``` 129 | 130 | ## Methods 131 | 132 | ```javascript 133 | slotMachine.play(); 134 | ``` 135 | 136 | ## Game options 137 | 138 | Customization and overriding defaults can be done using the following options: 139 | 140 | | Option | Description | Type | Default | 141 | |------------|----------------------------------------------------|-----------|---------------| 142 | | reelHeight | Reel background image height (in pixels) | Number | 1320 | 143 | | reelWidth | Reel background image width. | Number | 200 | 144 | | reelOffset | Reel background image vertical offset. | Number | 20 | 145 | | slotYAxis | Slot vertical axis rotation (in degrees). | Number | 0 | 146 | | animSpeed | Slot animation speed (in milliseconds) | Number | 1000 | 147 | | click2Spin | Add event to display to spin reels | Boolean | true | 148 | | rngFunc | Custom RNG between 0 (inclusive) and 1 (exclusive) | Function | `Math.random()` | 149 | | sounds | Audio clip URLs for reels animation events | Object | `{reelsBegin, reelsEnd}` | 150 | 151 | ## Callback 152 | 153 | This method returns an array of selected [reel symbols](#reels-configuration) that can be used to compute scoring, show animations, handle client interactions, etc.. 154 | 155 | ```javascript 156 | const callback = function(symbols) { 157 | if (symbols[0].title === 'cherry' && symbols[1].title === 'cherry' && symbols[2].title === 'cherry') { 158 | window.alert("You're a winner!"); 159 | } 160 | }; 161 | ``` 162 | 163 | ## Customizing symbols 164 | 165 | Creating a custom strip is fairly easy. What is most important is that each symbol, whether an image or blank space, contains a vertical `position` that can be measured by calculating the symbol center (in pixels) from the strip top. A [Photoshop example](https://raw.githubusercontent.com/nuxy/slot-machine-gen/master/images/reel-strip.psd) has been provided with this package for reference. 166 | 167 | ## Developers 168 | 169 | ### CLI options 170 | 171 | Run [ESLint](https://eslint.org) on project sources: 172 | 173 | $ npm run lint 174 | 175 | Transpile ES6 sources (using [Babel](https://babeljs.io)) and minify to a distribution: 176 | 177 | $ npm run build 178 | 179 | ## Contributions 180 | 181 | If you fix a bug, or have a code you want to contribute, please send a pull-request with your changes. (Note: Before committing your code please ensure that you are following the [Node.js style guide](https://github.com/felixge/node-style-guide)) 182 | 183 | ## Versioning 184 | 185 | This package is maintained under the [Semantic Versioning](https://semver.org) guidelines. 186 | 187 | ## License and Warranty 188 | 189 | This package is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. 190 | 191 | _slot-machine-gen_ is provided under the terms of the [MIT license](http://www.opensource.org/licenses/mit-license.php) 192 | 193 | ## Author 194 | 195 | [Marc S. Brooks](https://github.com/nuxy) 196 | -------------------------------------------------------------------------------- /src/slot-machine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Slot Machine Generator 3 | * Create an extremely biased, web-based slot machine game. 4 | * 5 | * Copyright 2020-2025, Marc S. Brooks (https://mbrooks.info) 6 | * Licensed under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * @param {Element} container 14 | * Containing HTML element. 15 | * 16 | * @param {Array} reels 17 | * Reel configuration. 18 | * 19 | * @param {Function} callback 20 | * Returns selected pay-line symbols. 21 | * 22 | * @param {Object} options 23 | * Configuration overrides (optional). 24 | */ 25 | function SlotMachine(container, reels, callback, options) { 26 | const self = this; 27 | 28 | const REEL_SEGMENT_TOTAL = 24; 29 | 30 | const defaults = { 31 | reelHeight: 1200, 32 | reelWidth: 200, 33 | reelOffset: 20, 34 | slotYAxis: 0, 35 | animSpeed: 1000, 36 | click2Spin: true, 37 | sounds: { 38 | reelsBegin: null, 39 | reelsEnd: null 40 | }, 41 | rngFunc: function() { 42 | 43 | // The weakest link. 44 | return Math.random(); 45 | } 46 | }; 47 | 48 | (function() { 49 | self.options = Object.assign(defaults, options); 50 | 51 | if (reels.length > 0) { 52 | initGame(); 53 | } else { 54 | throw new Error('Failed to initialize (missing reels)'); 55 | } 56 | })(); 57 | 58 | /** 59 | * Initialize a new game instance. 60 | */ 61 | function initGame() { 62 | container.setAttribute('aria-label', 'Slot machine'); 63 | 64 | createDisplayElm(); 65 | createSlotElm(); 66 | } 67 | 68 | /** 69 | * Create display elements. 70 | */ 71 | function createDisplayElm() { 72 | const div = document.createElement('div'); 73 | div.classList.add('display'); 74 | 75 | for (let i = 0; i < reels.length; i++) { 76 | const elm = document.createElement('div'); 77 | elm.classList.add('reel'); 78 | elm.setAttribute('role', 'none'); 79 | elm.style.transform = `rotateY(${self.options.slotYAxis}deg)`; 80 | 81 | div.appendChild(elm); 82 | } 83 | 84 | if (self.options.click2Spin) { 85 | const title = 'Click to spin'; 86 | 87 | // Add event to display to spin reels. 88 | div.addEventListener('click', spinReels); 89 | div.setAttribute('aria-label', title); 90 | div.setAttribute('role', 'button'); 91 | div.setAttribute('title', title); 92 | div.style.cursor = 'pointer'; 93 | } 94 | 95 | container.appendChild(div); 96 | } 97 | 98 | /** 99 | * Create slot elements. 100 | */ 101 | function createSlotElm() { 102 | const div = document.createElement('div'); 103 | div.classList.add('slots'); 104 | div.setAttribute('aria-label', 'Reels'); 105 | 106 | reels.forEach((reel, index) => { 107 | const elm = createReelElm(reel, reel.symbols[0].position); 108 | elm.setAttribute('aria-label', `Reel ${index + 1}`); 109 | 110 | div.appendChild(elm); 111 | }); 112 | 113 | container.appendChild(div); 114 | } 115 | 116 | /** 117 | * Create reel elements. 118 | * 119 | * @param {Object} config 120 | * Config options. 121 | * 122 | * @param {Number} startPos 123 | * Start position. 124 | * 125 | * @return {Element} 126 | */ 127 | function createReelElm(config, startPos = 0) { 128 | const div = document.createElement('div'); 129 | div.style.transform = `rotateY(${self.options.slotYAxis}deg)`; 130 | div.classList.add('reel'); 131 | 132 | const elm = createStripElm(config, config.symbols[0].position); 133 | 134 | config['element'] = elm; 135 | 136 | div.appendChild(elm); 137 | 138 | return div; 139 | } 140 | 141 | /** 142 | * Create strip elements (faux-panoramic animation). 143 | * 144 | * @param {Object} config 145 | * Config options. 146 | * 147 | * @param {Number} startPos 148 | * Start position. 149 | * 150 | * @return {Element} 151 | */ 152 | function createStripElm(config, startPos = 0) { 153 | const stripHeight = getStripHeight(); 154 | const stripWidth = getStripWidth(); 155 | 156 | const segmentDeg = 360 / REEL_SEGMENT_TOTAL; 157 | 158 | const transZ = Math.trunc( 159 | Math.tan(90 / Math.PI - segmentDeg) * (stripHeight * 0.5) * 4 160 | ); 161 | 162 | const marginTop = transZ + stripHeight / 2; 163 | 164 | const ul = document.createElement('ul'); 165 | ul.style.height = stripHeight + 'px'; 166 | ul.style.marginTop = marginTop + 'px'; 167 | ul.style.width = stripWidth + 'px'; 168 | ul.classList.add('strip'); 169 | 170 | for (let i = 0; i < REEL_SEGMENT_TOTAL; i++) { 171 | const li = document.createElement('li'); 172 | li.append(i.toString()); 173 | 174 | const imgPosY = getImagePosY(i, startPos); 175 | const rotateX = (REEL_SEGMENT_TOTAL * segmentDeg) - (i * segmentDeg); 176 | 177 | // Position image per the strip angle/container radius. 178 | li.style.background = `url(${config.imageSrc}) 0 ${imgPosY}px`; 179 | li.style.height = stripHeight + 'px'; 180 | li.style.width = stripWidth + 'px'; 181 | li.style.transform = `rotateX(${rotateX}deg) translateZ(${transZ}px)`; 182 | 183 | ul.appendChild(li); 184 | } 185 | 186 | return ul; 187 | } 188 | 189 | /** 190 | * Select a random symbol by weight. 191 | * 192 | * @param {Array} symbols 193 | * List of symbols. 194 | * 195 | * @return {Object} 196 | */ 197 | function selectRandSymbol(symbols) { 198 | let totalWeight = 0; 199 | 200 | const symbolTotal = symbols.length; 201 | 202 | for (let i = 0; i < symbolTotal; i++) { 203 | const symbol = symbols[i]; 204 | const weight = symbol.weight; 205 | 206 | totalWeight += weight; 207 | } 208 | 209 | let randNum = getRandom() * totalWeight; 210 | 211 | for (let j = 0; j < symbolTotal; j++) { 212 | const symbol = symbols[j]; 213 | const weight = symbol.weight; 214 | 215 | if (randNum < weight) { 216 | return symbol; 217 | } 218 | 219 | randNum -= weight; 220 | } 221 | } 222 | 223 | /** 224 | * Spin the reels and try your luck. 225 | */ 226 | function spinReels() { 227 | const payLine = []; 228 | 229 | if (callback) { 230 | 231 | // Delay callback until animations have stopped. 232 | payLine.push = function() { 233 | Array.prototype.push.apply(this, arguments); 234 | 235 | if (payLine.length === reels.length) { 236 | window.setTimeout(() => { 237 | self.isAnimating = false; 238 | 239 | callback(payLine); 240 | }, self.options.animSpeed); 241 | } 242 | }; 243 | } 244 | 245 | playSound(self.options.sounds.reelsBegin); 246 | 247 | reels.forEach(reel => { 248 | const selected = selectRandSymbol(reel.symbols); 249 | const startPos = selected.position; 250 | 251 | // Start the rotation animation. 252 | const elm = reel.element; 253 | elm.classList.remove('stop'); 254 | elm.classList.toggle('spin'); 255 | 256 | // Shift images to select position. 257 | elm.childNodes.forEach((li, index) => { 258 | li.style.backgroundPositionY = getImagePosY(index, startPos) + 'px'; 259 | }); 260 | 261 | // Randomly stop rotation animation. 262 | window.setTimeout(() => { 263 | elm.classList.replace('spin', 'stop'); 264 | 265 | playSound(self.options.sounds.reelsEnd); 266 | 267 | payLine.push(selected); 268 | }, self.options.animSpeed * getRandomInt(1, 4)); 269 | }); 270 | } 271 | 272 | /** 273 | * Get random number between 0 (inclusive) and 1 (exclusive). 274 | * 275 | * @return {number} 276 | */ 277 | function getRandom() { 278 | return self.options.rngFunc(); 279 | } 280 | 281 | /** 282 | * Get random integer between two values. 283 | * 284 | * @param {Number} min 285 | * Minimum value (default: 0). 286 | * 287 | * @param {Number} max 288 | * Maximum value (default: 10). 289 | * 290 | * @return {Number} 291 | */ 292 | function getRandomInt(min = 1, max = 10) { 293 | const minNum = Math.ceil(min); 294 | const maxNum = Math.floor(max); 295 | 296 | return Math.floor(getRandom() * (Math.floor(maxNum) - minNum)) + minNum; 297 | } 298 | 299 | /** 300 | * Calculate the strip background position. 301 | * 302 | * @param {Number} index 303 | * Strip symbol index. 304 | * 305 | * @param {Number} position 306 | * Strip target position. 307 | * 308 | * @return {Number} 309 | */ 310 | function getImagePosY(index, position) { 311 | return -Math.abs( 312 | (getStripHeight() * index) + (position - self.options.reelOffset) 313 | ); 314 | } 315 | 316 | /** 317 | * Calculate the strip height. 318 | * 319 | * @return {Number} 320 | */ 321 | function getStripHeight() { 322 | return self.options.reelHeight / REEL_SEGMENT_TOTAL; 323 | } 324 | 325 | /** 326 | * Calculate the strip width. 327 | * 328 | * @return {Number} 329 | */ 330 | function getStripWidth() { 331 | return self.options.reelWidth; 332 | } 333 | 334 | /** 335 | * Play the audio clip. 336 | * 337 | * @param {String} url 338 | * Audio file URL. 339 | */ 340 | function playSound(url) { 341 | if (url) { 342 | const audio = new Audio(); 343 | audio.src = url; 344 | audio.onerror = () => console.warn(`Failed to load audio: ${url}`); 345 | audio.play(); 346 | } 347 | } 348 | 349 | /** 350 | * Dispatch game actions. 351 | * 352 | * @param {Function} func 353 | * Function to execute. 354 | */ 355 | function dispatch(func) { 356 | if (!self.isAnimating) { 357 | self.isAnimating = true; 358 | 359 | func.call(self); 360 | } 361 | } 362 | 363 | /** 364 | * Protected members. 365 | */ 366 | self.play = function() { 367 | dispatch(spinReels); 368 | }; 369 | 370 | return self; 371 | } 372 | 373 | /** 374 | * Set global/exportable instance, where supported. 375 | */ 376 | window.slotMachine = function(container, reels, callback, options) { 377 | return new SlotMachine(container, reels, callback, options); 378 | }; 379 | 380 | if (typeof module !== 'undefined' && module.exports) { 381 | module.exports = SlotMachine; 382 | } 383 | --------------------------------------------------------------------------------