Spaces:
Running
written in react i want a 3 reel slot machine that uses this logic Terms to understand
Browse filesReels. A reel is the vertical spinning thingie. Modern video slots have five reels, while older electromechanical slots have three reels.
Symbols. The pictures on the reels. The player gets paid if s/he lines up a winning combination of symbols.
Stops. The stops are each place on the reel that shows a symbol. Here's a tiny reel with just ten stops: apple, banana, cherry, cherry, banana, apple, apple, cherry cherry, apple. This reel has three symbols (apple, banana, cherry), but ten stops (places that a spin can stop on).
Paylines. A line that goes from left to right through all the reels, going through one symbol on each reel. This is easier to see that to understand from text.
Paytable. A list of which symbol combinations pay how much. e.g., Three apples might pay 50 coins.
Par sheet. The "blueprint" for a slot machine. It's a list of the symbols on each reel, and a paytable. The combination of the reels and the paytable is what determines the RTP.
RTP / Payback. This is how much the machine pays back over an infinite amount of play. RTP stands for "Return to Player". Most real-money online slots pay back around 96%. Here's my detailed article about RTP.
RNG. Random number generator. This is an algorithm that picks random numbers. Note that the very nature of using a formula to pick numbers means that the numbers aren't perfectly random, so computer RNG's are actually "pseudo random numbers", so an RNG is often called a PRNG. Even so, good PRNG's pick numbers that are indistinguishable from truly random numbers.
Preparation
Find, make, or buy a par sheet. You have various options for getting a par sheet.
FIND A FREE ONE. I have several that you can use for free.
MAKE ONE. For a simple slot machine, it's not hard. See my article on How Slot Machines Work to understand the simple math.
BUY ONE. You might need features that you can't find in a free par sheet and that are beyond your ability to design yourself (usually a specific RTP). In that case you can hire a mathematician or slot-math specialist to make one for you. I offer this service if you need it.
Choose an RNG. If your game isn't played for real money, you can use whatever RNG comes with your programming language; it's good enough. If money is on the line then you need a cryptographically-secure PRNG, called a CSPRNG. Examples:
Javascript: Crytpo.getRandomValues
Java: java.security.SecureRandom
Unix: /dev/random
Perl: Crypt::Random
Decide on client-based or server-based. If there's no money on the line, put all of the code on the client (e.g., web browser). That way the game isn't using any of your server resources, and the game will work even if the player's Internet connection drops. If there's money on the line, then everything should be on the server except the GUI. It's a security risk to have the RNG or the math for the player's balance on the client. The server should generate the random result and send that to the client, and the server should also determine how much the player won and add that to her/his account.
Slot machine code (logic) for client-based games
Get the stop for the first reel. To do that, pick a random number between 1 and the number of stops on the reel (e.g., 128).
Repeat step 1 until you have all five stops (or all three stops, if a three-reel game).
Loop through all the paylines, finding out how much was won on each payline for the symbols on that line.
Do the animation, having it stop on each of the five stops your RNG picked.
Notify the player of how much they won, as per the paytable.
Slot machine code (logic) for server-based games
When a player creates an account, create a token and store it in the user's database record, to verify the player's identity when the player's client sends request to the server. This prevents hackers from sending bogus requests by trying all possible player IDs. The token is not the player's personal password.
Store that token as a cookie in the player's browser or app, so the client can send it to the server along with the player ID.
When the user loads a game, the client sends an encrypted request to the server, with the player's ID and token. The server fetches the player's $/€ balance and sends that to the client to display in the game.
When the player clicks the Spin button, send a request to the server, encrypted, with the player ID, the player's token, the game ID (which slot), the amount bet, and which lines were selected.
The server verifies that the player ID and the player's token match.
The server verifies that there's enough money in the account to make the bet. There should be, because the client shouldn't allow a bet to be made with insufficient funds, but a hacker could send a request for a bet that exceeds the player balance. That's why we check the balance first.
Get the stop for the first reel. To do that, pick a random number between 1 and the number of stops on the reel (e.g., 128).
Repeat step 1 until you have all five stops (or all three stops, if a three-reel game).
Loop through all the paylines, finding out how much was won on each payline for the symbols on that line.
Log the result. Logs might be required by regulators, and are certainly a good idea to be able to check players' activity and to make sure the games are working properly (e.g., returning expected RTP).
When logging the result, set a flag to indicate that the player hasn't yet seen the results. That way we'll be able to show the results if the player gets disconnected before seeing them.
Update the player's account balance in the database according to how much they won.
Send instructions to the client to do the animation (having it stop on each of the five stops your server-based RNG picked), show the winning lines, and tell the player how much they won.
Clear the flag that says the player hasn't seen the result yet, since by now they've seen the results.
When a player logs in check to see if the "not seen result yet" flag has been set, and if so, then run the animation, notify the user of how much they won, and clear the flag.
Progressive Jackpots
The PAR sheet tells you the progressive contribution. e.g., If it's 2%, then grow the jackpot meter by 2% of each bet placed.
"Must-Hit-By" Progressive Jackpots (MHB)
Set the secret value that the jackpot will be awarded at, by picking any value between the reseed value (jackpot starting value) and the maximum value (the must-hit-by value). The value doesn't have to be chosen uniformly from all values between min and max, it can be biased towards the max value (which is how most slot makers do it).
Complete working example (Javascript)
Here's the complete code for a Javascript slot machine (sans graphics). This is for a 3-reel, electromechanical style. The principle is the same for a five-reel slot. For five reels, just use two extra reels, and put symbols on every stop (instead of a blank every other symbol), as per a par sheet .
Run this code
// Javascript slot machine example, 3/2023 by Michael Bluejay
// For a 3-reel, electromechanical-type slot.
// For a five reel video slot, just add in two extra reels, and put symbols on every stop
// (instead of a blank every other stop), as per a par sheet.
// Sample par sheets at: https://easy.vegas/games/slots/par-sheets
// The slot in this code uses the 'Easy Vegas' par sheet #1 (99%) from the above link.
// MAIN LOGIC
buildReels()
defineTripletPays()
calculateRTP()
testSpins(1000000)
// For a live game, every time the player presses the Spin button, you'd simply call spin()
// FUNCTIONS
function buildReels() {
// -------------------
// BUILD THE REELS
// -------------------
// There are 3 ways to create the reels:
// (1) Manually type out the string. Clear, but tedious, & have to do it again for each new par sheet.
// (2) Write a separate pgm to randomize a string of the symbols, then paste it into your code here.
// (3) Create it programmatically, distributing the symbols fairly equally. (least error-prone, and re-usable for other par sheets)
// We'll use option #3 here. The reel created by our algo below is:
// J-7-3-2-1-C-2-1-C-7-3-1-J-2-1-3-1-C-7-2-1-1-J-3-2-1-C-7-1-3-2-1-C-J-7-2-1-3-1-1-C-7-2-1-J-3-1-2-1-C-7-3-1-2-1-J-7-3-1-C-2-1-1-1-
// Builds the reel one character at a time, checking if each symbol is underrepresented at each point, and adding it if so.
// Jackpot, Red 7, Triple Bar, Double Bar, Single Bar, Cherry
symbols = ['J','7','3','2','1','C'] // We'll handle blanks separately, later.
weights = [6,8,9,11,22,8] // Number of times each symbol appears on the reel
size = 0 // We'll count the total number of symbol occurrences
for (i=0; i < symbols.length; i++) size += weights[i]
reelString = ""
while (reelString.length <64) {
for (symbolIndex = 0; symbolIndex < symbols.length; symbolIndex++) {
symbol = symbols[symbolIndex]
if (count() < weights[symbolIndex] && count() <= weights[symbolIndex]/size * reelString.length)
reelString+= symbols[symbolIndex]
}
}
// INSERT BLANKS BETWEEN THE SYMBOLS
reel = ''
for (i=0; i < reelString.length; i++)
reel += reelString[i] + '-'
console.log(reel)
// We'll use this reel for all 3 reels, b/c in this game the reels are identical.
}
// Used above. Counts the number of times a symbol appears on the reel.
function count() { return reelString.split(symbol).length-1 }
function defineTripletPays() {
// Using four digits for the pays so we need less code to extract it later
triplets = `
JJJ 1199
777 0200
333 0100
222 0090
111 0040
CCC 0040
`
// Note, pays for any bar (10) and 1 or 2 cherries (1 or 5) are hardcoded below.
}
function calculateRTP() {
// Loops through all possible combinations of symbols (called a "machine cycle") and tallies the pays.
// RTP for simple slots like this one can be calculated by hand, but if a slot has wilds (and most do),
// for which calculations are difficult and error-prone,
// looping through a machine cycle is the preferred way to verify the RTP.
wins =
- README.md +6 -4
- index.html +446 -18
|
@@ -1,10 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: red
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: slots
|
| 3 |
+
emoji: 🐳
|
| 4 |
+
colorFrom: red
|
| 5 |
colorTo: red
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
@@ -1,19 +1,447 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crane Slot Machine</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
|
| 11 |
+
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
| 12 |
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
| 13 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 14 |
+
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Luckiest+Guy&display=swap" rel="stylesheet">
|
| 16 |
+
<style>
|
| 17 |
+
.crane-bg {
|
| 18 |
+
background-image: url('https://images.unsplash.com/photo-1605106702734-205df224ecce?q=80&w=2070');
|
| 19 |
+
background-size: cover;
|
| 20 |
+
background-position: center;
|
| 21 |
+
}
|
| 22 |
+
.slot-machine {
|
| 23 |
+
background: linear-gradient(135deg, #ff0000 0%, #990000 100%);
|
| 24 |
+
box-shadow: 0 0 30px rgba(255, 215, 0, 0.7);
|
| 25 |
+
}
|
| 26 |
+
.reel {
|
| 27 |
+
background: linear-gradient(180deg, #f8f8f8 0%, #e0e0e0 100%);
|
| 28 |
+
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
|
| 29 |
+
}
|
| 30 |
+
.symbol {
|
| 31 |
+
font-family: 'Luckiest Guy', cursive;
|
| 32 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 33 |
+
}
|
| 34 |
+
.bonus-game {
|
| 35 |
+
background: rgba(0,0,0,0.8);
|
| 36 |
+
backdrop-filter: blur(5px);
|
| 37 |
+
}
|
| 38 |
+
.crane-claw {
|
| 39 |
+
transition: all 0.3s ease;
|
| 40 |
+
}
|
| 41 |
+
.win-animation {
|
| 42 |
+
animation: pulse 0.5s infinite alternate;
|
| 43 |
+
}
|
| 44 |
+
@keyframes pulse {
|
| 45 |
+
from { transform: scale(1); }
|
| 46 |
+
to { transform: scale(1.05); }
|
| 47 |
+
}
|
| 48 |
+
</style>
|
| 49 |
+
</head>
|
| 50 |
+
<body class="crane-bg min-h-screen flex items-center justify-center p-4">
|
| 51 |
+
<div id="root"></div>
|
| 52 |
+
|
| 53 |
+
<script type="text/babel">
|
| 54 |
+
const { useState, useEffect, useRef } = React;
|
| 55 |
+
|
| 56 |
+
const SlotMachine = () => {
|
| 57 |
+
const [reels, setReels] = useState([[],[],[]]);
|
| 58 |
+
const [spinning, setSpinning] = useState(false);
|
| 59 |
+
const [balance, setBalance] = useState(1000);
|
| 60 |
+
const [bet, setBet] = useState(10);
|
| 61 |
+
const [winAmount, setWinAmount] = useState(0);
|
| 62 |
+
const [bonusGame, setBonusGame] = useState(false);
|
| 63 |
+
const [bonusPrize, setBonusPrize] = useState(null);
|
| 64 |
+
const [walletConnected, setWalletConnected] = useState(false);
|
| 65 |
+
const [walletAddress, setWalletAddress] = useState('');
|
| 66 |
+
const [provider, setProvider] = useState(null);
|
| 67 |
+
|
| 68 |
+
const reelSymbols = ['🍒', '🍋', '🍊', '🍇', '🔔', '7️⃣', '💰', '🎁'];
|
| 69 |
+
const reelStops = 64;
|
| 70 |
+
|
| 71 |
+
const bonusItems = [
|
| 72 |
+
{ id: 1, value: 50, position: { x: 20, y: 70 } },
|
| 73 |
+
{ id: 2, value: 100, position: { x: 50, y: 70 } },
|
| 74 |
+
{ id: 3, value: 200, position: { x: 80, y: 70 } },
|
| 75 |
+
{ id: 4, value: 'JACKPOT', position: { x: 35, y: 60 } },
|
| 76 |
+
{ id: 5, value: 75, position: { x: 65, y: 60 } }
|
| 77 |
+
];
|
| 78 |
+
|
| 79 |
+
const clawPosition = useRef({ x: 50, y: 10 });
|
| 80 |
+
const clawGrabbing = useRef(false);
|
| 81 |
+
const clawDropping = useRef(false);
|
| 82 |
+
|
| 83 |
+
// Initialize reels
|
| 84 |
+
useEffect(() => {
|
| 85 |
+
const initialReels = [[], [], []];
|
| 86 |
+
for (let i = 0; i < 3; i++) {
|
| 87 |
+
for (let j = 0; j < reelStops; j++) {
|
| 88 |
+
initialReels[i].push(reelSymbols[Math.floor(Math.random() * reelSymbols.length)]);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
setReels(initialReels);
|
| 92 |
+
}, []);
|
| 93 |
+
|
| 94 |
+
const connectWallet = async () => {
|
| 95 |
+
if (window.ethereum) {
|
| 96 |
+
try {
|
| 97 |
+
const provider = new ethers.providers.Web3Provider(window.ethereum);
|
| 98 |
+
await provider.send("eth_requestAccounts", []);
|
| 99 |
+
const signer = provider.getSigner();
|
| 100 |
+
const address = await signer.getAddress();
|
| 101 |
+
|
| 102 |
+
setProvider(provider);
|
| 103 |
+
setWalletAddress(`${address.substring(0, 6)}...${address.substring(address.length - 4)}`);
|
| 104 |
+
setWalletConnected(true);
|
| 105 |
+
} catch (error) {
|
| 106 |
+
console.error("Error connecting wallet:", error);
|
| 107 |
+
}
|
| 108 |
+
} else {
|
| 109 |
+
alert("Please install MetaMask or another Ethereum wallet!");
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
const spin = () => {
|
| 114 |
+
if (spinning || balance < bet) return;
|
| 115 |
+
|
| 116 |
+
setSpinning(true);
|
| 117 |
+
setWinAmount(0);
|
| 118 |
+
setBalance(prev => prev - bet);
|
| 119 |
+
|
| 120 |
+
// Simulate spinning animation
|
| 121 |
+
const spinDuration = 2000;
|
| 122 |
+
const spinInterval = 100;
|
| 123 |
+
const spins = spinDuration / spinInterval;
|
| 124 |
+
|
| 125 |
+
let spinCount = 0;
|
| 126 |
+
const spinIntervalId = setInterval(() => {
|
| 127 |
+
const newReels = [...reels];
|
| 128 |
+
for (let i = 0; i < 3; i++) {
|
| 129 |
+
const randomIndex = Math.floor(Math.random() * reelStops);
|
| 130 |
+
newReels[i] = [...newReels[i].slice(randomIndex), ...newReels[i].slice(0, randomIndex)];
|
| 131 |
+
}
|
| 132 |
+
setReels(newReels);
|
| 133 |
+
spinCount++;
|
| 134 |
+
|
| 135 |
+
if (spinCount >= spins) {
|
| 136 |
+
clearInterval(spinIntervalId);
|
| 137 |
+
checkWin();
|
| 138 |
+
}
|
| 139 |
+
}, spinInterval);
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
const checkWin = () => {
|
| 143 |
+
// Get the middle symbols of each reel
|
| 144 |
+
const results = [
|
| 145 |
+
reels[0][Math.floor(reelStops / 2)],
|
| 146 |
+
reels[1][Math.floor(reelStops / 2)],
|
| 147 |
+
reels[2][Math.floor(reelStops / 2)]
|
| 148 |
+
];
|
| 149 |
+
|
| 150 |
+
// Simple win logic - adjust as needed
|
| 151 |
+
let win = 0;
|
| 152 |
+
|
| 153 |
+
// Check for three matching symbols
|
| 154 |
+
if (results[0] === results[1] && results[1] === results[2]) {
|
| 155 |
+
switch(results[0]) {
|
| 156 |
+
case '🍒': win = bet * 5; break;
|
| 157 |
+
case '🍋': win = bet * 10; break;
|
| 158 |
+
case '🍊': win = bet * 15; break;
|
| 159 |
+
case '🍇': win = bet * 20; break;
|
| 160 |
+
case '🔔': win = bet * 25; break;
|
| 161 |
+
case '7️⃣': win = bet * 50; break;
|
| 162 |
+
case '💰': win = bet * 100; break;
|
| 163 |
+
case '🎁':
|
| 164 |
+
win = bet * 5;
|
| 165 |
+
// Trigger bonus game
|
| 166 |
+
setTimeout(() => {
|
| 167 |
+
setBonusGame(true);
|
| 168 |
+
}, 1500);
|
| 169 |
+
break;
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
// Check for two matching symbols
|
| 173 |
+
else if (results[0] === results[1] || results[1] === results[2] || results[0] === results[2]) {
|
| 174 |
+
win = bet * 2;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
setWinAmount(win);
|
| 178 |
+
if (win > 0) {
|
| 179 |
+
setBalance(prev => prev + win);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
setSpinning(false);
|
| 183 |
+
};
|
| 184 |
+
|
| 185 |
+
const moveClaw = (direction) => {
|
| 186 |
+
if (clawGrabbing.current || clawDropping.current) return;
|
| 187 |
+
|
| 188 |
+
switch(direction) {
|
| 189 |
+
case 'left':
|
| 190 |
+
if (clawPosition.current.x > 10) clawPosition.current.x -= 5;
|
| 191 |
+
break;
|
| 192 |
+
case 'right':
|
| 193 |
+
if (clawPosition.current.x < 90) clawPosition.current.x += 5;
|
| 194 |
+
break;
|
| 195 |
+
case 'down':
|
| 196 |
+
if (!clawGrabbing.current && clawPosition.current.y < 80) {
|
| 197 |
+
clawGrabbing.current = true;
|
| 198 |
+
let y = clawPosition.current.y;
|
| 199 |
+
const dropInterval = setInterval(() => {
|
| 200 |
+
y += 2;
|
| 201 |
+
clawPosition.current.y = y;
|
| 202 |
+
|
| 203 |
+
if (y >= 70) {
|
| 204 |
+
clearInterval(dropInterval);
|
| 205 |
+
grabPrize();
|
| 206 |
+
}
|
| 207 |
+
}, 50);
|
| 208 |
+
}
|
| 209 |
+
break;
|
| 210 |
+
}
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
const grabPrize = () => {
|
| 214 |
+
// Find which prize is under the claw
|
| 215 |
+
const clawX = clawPosition.current.x;
|
| 216 |
+
const clawY = clawPosition.current.y;
|
| 217 |
+
|
| 218 |
+
const prize = bonusItems.find(item => {
|
| 219 |
+
return Math.abs(item.position.x - clawX) < 10 &&
|
| 220 |
+
Math.abs(item.position.y - clawY) < 10;
|
| 221 |
+
});
|
| 222 |
+
|
| 223 |
+
if (prize) {
|
| 224 |
+
setBonusPrize(prize);
|
| 225 |
+
|
| 226 |
+
// Animate claw going back up with prize
|
| 227 |
+
clawDropping.current = true;
|
| 228 |
+
let y = clawPosition.current.y;
|
| 229 |
+
const riseInterval = setInterval(() => {
|
| 230 |
+
y -= 2;
|
| 231 |
+
clawPosition.current.y = y;
|
| 232 |
+
|
| 233 |
+
if (y <= 10) {
|
| 234 |
+
clearInterval(riseInterval);
|
| 235 |
+
clawGrabbing.current = false;
|
| 236 |
+
clawDropping.current = false;
|
| 237 |
+
|
| 238 |
+
// Award prize
|
| 239 |
+
if (prize.value === 'JACKPOT') {
|
| 240 |
+
setWinAmount(bet * 500);
|
| 241 |
+
setBalance(prev => prev + bet * 500);
|
| 242 |
+
} else {
|
| 243 |
+
setWinAmount(prize.value);
|
| 244 |
+
setBalance(prev => prev + prize.value);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// Close bonus game after delay
|
| 248 |
+
setTimeout(() => {
|
| 249 |
+
setBonusGame(false);
|
| 250 |
+
setBonusPrize(null);
|
| 251 |
+
clawPosition.current = { x: 50, y: 10 };
|
| 252 |
+
}, 2000);
|
| 253 |
+
}
|
| 254 |
+
}, 50);
|
| 255 |
+
} else {
|
| 256 |
+
// No prize grabbed, return to top
|
| 257 |
+
clawGrabbing.current = false;
|
| 258 |
+
let y = clawPosition.current.y;
|
| 259 |
+
const riseInterval = setInterval(() => {
|
| 260 |
+
y -= 2;
|
| 261 |
+
clawPosition.current.y = y;
|
| 262 |
+
|
| 263 |
+
if (y <= 10) {
|
| 264 |
+
clearInterval(riseInterval);
|
| 265 |
+
}
|
| 266 |
+
}, 50);
|
| 267 |
+
}
|
| 268 |
+
};
|
| 269 |
+
|
| 270 |
+
return (
|
| 271 |
+
<div className="relative w-full max-w-3xl">
|
| 272 |
+
{/* Main Slot Machine */}
|
| 273 |
+
<div className={`slot-machine rounded-2xl p-6 shadow-2xl ${bonusGame ? 'opacity-30' : ''}`}>
|
| 274 |
+
<div className="flex justify-between items-center mb-6">
|
| 275 |
+
<div className="bg-black bg-opacity-70 text-yellow-400 px-4 py-2 rounded-lg">
|
| 276 |
+
<span className="font-bold">BALANCE: </span>
|
| 277 |
+
<span className="text-white">{balance}</span>
|
| 278 |
+
</div>
|
| 279 |
+
|
| 280 |
+
{walletConnected ? (
|
| 281 |
+
<div className="bg-green-700 text-white px-4 py-2 rounded-lg flex items-center">
|
| 282 |
+
<i data-feather="check-circle" className="mr-2"></i>
|
| 283 |
+
{walletAddress}
|
| 284 |
+
</div>
|
| 285 |
+
) : (
|
| 286 |
+
<button
|
| 287 |
+
onClick={connectWallet}
|
| 288 |
+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"
|
| 289 |
+
>
|
| 290 |
+
<i data-feather="wallet" className="mr-2"></i>
|
| 291 |
+
Connect Wallet
|
| 292 |
+
</button>
|
| 293 |
+
)}
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
{/* Reels */}
|
| 297 |
+
<div className="flex justify-center mb-6">
|
| 298 |
+
{[0, 1, 2].map((reelIndex) => (
|
| 299 |
+
<div key={reelIndex} className="reel mx-1 w-24 h-48 rounded-lg overflow-hidden relative">
|
| 300 |
+
<div className={`absolute w-full transition-transform duration-100 ${spinning ? 'animate-spin' : ''}`}>
|
| 301 |
+
{reels[reelIndex]?.map((symbol, index) => (
|
| 302 |
+
<div
|
| 303 |
+
key={index}
|
| 304 |
+
className="symbol flex items-center justify-center text-4xl h-16 border-b border-gray-300"
|
| 305 |
+
>
|
| 306 |
+
{symbol}
|
| 307 |
+
</div>
|
| 308 |
+
))}
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
))}
|
| 312 |
+
</div>
|
| 313 |
+
|
| 314 |
+
{/* Controls */}
|
| 315 |
+
<div className="flex justify-between items-center">
|
| 316 |
+
<div className="bg-black bg-opacity-70 text-white px-4 py-2 rounded-lg">
|
| 317 |
+
<div className="flex items-center">
|
| 318 |
+
<span className="mr-2">BET:</span>
|
| 319 |
+
<button
|
| 320 |
+
onClick={() => setBet(Math.max(1, bet - 1))}
|
| 321 |
+
className="bg-gray-600 hover:bg-gray-700 w-8 h-8 rounded-l flex items-center justify-center"
|
| 322 |
+
>
|
| 323 |
+
-
|
| 324 |
+
</button>
|
| 325 |
+
<div className="bg-gray-800 w-12 text-center">{bet}</div>
|
| 326 |
+
<button
|
| 327 |
+
onClick={() => setBet(bet + 1)}
|
| 328 |
+
className="bg-gray-600 hover:bg-gray-700 w-8 h-8 rounded-r flex items-center justify-center"
|
| 329 |
+
>
|
| 330 |
+
+
|
| 331 |
+
</button>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
<button
|
| 336 |
+
onClick={spin}
|
| 337 |
+
disabled={spinning || balance < bet}
|
| 338 |
+
className={`spin-button bg-yellow-500 hover:bg-yellow-600 text-black font-bold py-3 px-8 rounded-full text-xl flex items-center ${spinning ? 'opacity-50' : ''}`}
|
| 339 |
+
>
|
| 340 |
+
<i data-feather="rotate-cw" className="mr-2"></i>
|
| 341 |
+
SPIN
|
| 342 |
+
</button>
|
| 343 |
+
|
| 344 |
+
<div className={`bg-black bg-opacity-70 text-green-400 px-4 py-2 rounded-lg transition-opacity ${winAmount > 0 ? 'opacity-100 win-animation' : 'opacity-0'}`}>
|
| 345 |
+
WIN: {winAmount}
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
{/* Bonus Game */}
|
| 351 |
+
{bonusGame && (
|
| 352 |
+
<div className="bonus-game absolute inset-0 flex flex-col items-center justify-center rounded-2xl p-6">
|
| 353 |
+
<h2 className="text-yellow-400 text-3xl font-bold mb-4">BONUS GAME!</h2>
|
| 354 |
+
<p className="text-white mb-6">Use the controls to grab a prize!</p>
|
| 355 |
+
|
| 356 |
+
<div className="relative w-full h-64 bg-gray-900 rounded-lg mb-4">
|
| 357 |
+
{/* Crane Claw */}
|
| 358 |
+
<div
|
| 359 |
+
className="crane-claw absolute w-8 h-8 bg-red-600 rounded-full z-10"
|
| 360 |
+
style={{
|
| 361 |
+
left: `${clawPosition.current.x}%`,
|
| 362 |
+
top: `${clawPosition.current.y}%`,
|
| 363 |
+
transform: 'translate(-50%, -50%)'
|
| 364 |
+
}}
|
| 365 |
+
>
|
| 366 |
+
<div className="absolute w-1 h-12 bg-gray-700 -top-12 left-1/2 transform -translate-x-1/2"></div>
|
| 367 |
+
</div>
|
| 368 |
+
|
| 369 |
+
{/* Bonus Items */}
|
| 370 |
+
{bonusItems.map(item => (
|
| 371 |
+
<div
|
| 372 |
+
key={item.id}
|
| 373 |
+
className={`absolute w-12 h-12 flex items-center justify-center text-2xl rounded-lg ${bonusPrize?.id === item.id ? 'bg-yellow-400' : 'bg-blue-500'}`}
|
| 374 |
+
style={{
|
| 375 |
+
left: `${item.position.x}%`,
|
| 376 |
+
top: `${item.position.y}%`,
|
| 377 |
+
transform: 'translate(-50%, -50%)'
|
| 378 |
+
}}
|
| 379 |
+
>
|
| 380 |
+
{item.value === 'JACKPOT' ? '💰' : item.value}
|
| 381 |
+
</div>
|
| 382 |
+
))}
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
{/* Crane Controls */}
|
| 386 |
+
<div className="flex space-x-4">
|
| 387 |
+
<button
|
| 388 |
+
onClick={() => moveClaw('left')}
|
| 389 |
+
className="bg-gray-700 hover:bg-gray-600 text-white w-12 h-12 rounded-full flex items-center justify-center"
|
| 390 |
+
>
|
| 391 |
+
<i data-feather="arrow-left"></i>
|
| 392 |
+
</button>
|
| 393 |
+
<button
|
| 394 |
+
onClick={() => moveClaw('down')}
|
| 395 |
+
className="bg-red-600 hover:bg-red-700 text-white w-12 h-12 rounded-full flex items-center justify-center"
|
| 396 |
+
>
|
| 397 |
+
<i data-feather="arrow-down"></i>
|
| 398 |
+
</button>
|
| 399 |
+
<button
|
| 400 |
+
onClick={() => moveClaw('right')}
|
| 401 |
+
className="bg-gray-700 hover:bg-gray-600 text-white w-12 h-12 rounded-full flex items-center justify-center"
|
| 402 |
+
>
|
| 403 |
+
<i data-feather="arrow-right"></i>
|
| 404 |
+
</button>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
)}
|
| 408 |
+
</div>
|
| 409 |
+
);
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 413 |
+
root.render(<SlotMachine />);
|
| 414 |
+
|
| 415 |
+
// Initialize feather icons
|
| 416 |
+
feather.replace();
|
| 417 |
+
</script>
|
| 418 |
+
</body>
|
| 419 |
</html>
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
This implementation includes:
|
| 423 |
+
|
| 424 |
+
1. A 3-reel slot machine with animated spinning reels
|
| 425 |
+
2. Crane machine theme with appropriate styling
|
| 426 |
+
3. Bonus game triggered by landing on the gift symbol
|
| 427 |
+
4. Wallet integration using MetaMask/Web3
|
| 428 |
+
5. Bet adjustment controls
|
| 429 |
+
6. Win animations and balance tracking
|
| 430 |
+
7. Responsive design using TailwindCSS
|
| 431 |
+
8. Interactive crane controls for the bonus game
|
| 432 |
+
|
| 433 |
+
The slot machine features:
|
| 434 |
+
- 8 different symbols with varying payouts
|
| 435 |
+
- Random reel stops generation
|
| 436 |
+
- Win detection logic (both for regular spins and bonus game)
|
| 437 |
+
- Balance tracking with bet adjustments
|
| 438 |
+
- Ethereum wallet integration for cryptocurrency transactions
|
| 439 |
+
|
| 440 |
+
The bonus game allows players to control a crane to grab prizes when triggered. The game includes a jackpot and several other prize values.
|
| 441 |
+
|
| 442 |
+
Note: For a production environment, you would want to:
|
| 443 |
+
1. Implement proper server-side RNG for fairness
|
| 444 |
+
2. Add more sophisticated win detection logic
|
| 445 |
+
3. Include proper wallet transaction handling
|
| 446 |
+
4. Add sound effects
|
| 447 |
+
5. Implement proper security measures for wallet integration
|