add: absorb uno-online as regular subdirectory
UNO card game web app (Node.js/React) with Miku bot integration. Previously an independent git repo (fork of mizanxali/uno-online). Removed .git/ and absorbed into main repo for unified tracking. Includes bot integration code: botActionExecutor, cardParser, gameStateBuilder, and server-side bot action support. 37 files, node_modules excluded via local .gitignore.
This commit is contained in:
20304
uno-online/client/package-lock.json
generated
Normal file
20304
uno-online/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
uno-online/client/package.json
Normal file
41
uno-online/client/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "uno-online",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.7.0",
|
||||
"query-string": "^6.14.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.2",
|
||||
"socket.io-client": "^3.1.1",
|
||||
"use-sound": "^2.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
uno-online/client/public/favicon.ico
Normal file
BIN
uno-online/client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
50
uno-online/client/public/index.html
Normal file
50
uno-online/client/public/index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Two player online game of UNO."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>UNO Online</title>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="text/javascript">
|
||||
window.onbeforeunload = function() {
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
25
uno-online/client/public/manifest.json
Normal file
25
uno-online/client/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
uno-online/client/public/robots.txt
Normal file
3
uno-online/client/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
387
uno-online/client/src/App.css
Normal file
387
uno-online/client/src/App.css
Normal file
@@ -0,0 +1,387 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Homepage */
|
||||
.Homepage {
|
||||
background-image: url('./assets/Landing-Page.gif');
|
||||
background-size: cover;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.homepage-menu {
|
||||
position: relative;
|
||||
top: 120px;
|
||||
}
|
||||
|
||||
.homepage-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.homepage-form>h1 {
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.homepage-join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.homepage-join>input {
|
||||
font-size: 15px;
|
||||
width: 150px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
/* Game.js parent div */
|
||||
.Game {
|
||||
background-size: cover;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Game.js Background */
|
||||
.backgroundColorR {
|
||||
background-image: url('./assets/backgrounds/bgR.png');
|
||||
}
|
||||
|
||||
.backgroundColorG {
|
||||
background-image: url('./assets/backgrounds/bgG.png');
|
||||
}
|
||||
|
||||
.backgroundColorB {
|
||||
background-image: url('./assets/backgrounds/bgB.png');
|
||||
}
|
||||
|
||||
.backgroundColorY {
|
||||
background-image: url('./assets/backgrounds/bgY.png');
|
||||
}
|
||||
|
||||
/* UNO Cards */
|
||||
.Card {
|
||||
width: 6rem;
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Game.js Top Row */
|
||||
.topInfo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 60px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.topInfo>img {
|
||||
width: 7%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.topInfo>h1 {
|
||||
margin-top: 10px;
|
||||
font-size: 1.8rem;
|
||||
padding-top: 2%;
|
||||
}
|
||||
|
||||
.topInfoText {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Player Decks */
|
||||
.player1Deck {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player1Deck>img {
|
||||
transition: transform 350ms;
|
||||
}
|
||||
|
||||
.player1Deck>img:hover {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.player2Deck {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player2Deck>img {
|
||||
transition: transform 350ms;
|
||||
}
|
||||
|
||||
.player2Deck>img:hover {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.playerDeckText {
|
||||
font-size: 2rem;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
/* Game.js Middle Row */
|
||||
.middleInfo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.middleInfo>button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Game Buttons */
|
||||
.game-button {
|
||||
position: relative;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none !important;
|
||||
outline: none !important;
|
||||
font-family: 'Carter One', sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5em;
|
||||
letter-spacing: .1em;
|
||||
text-shadow: 2px 2px 1px #0066a2, -2px 2px 1px #0066a2, 2px -2px 1px #0066a2, -2px -2px 1px #0066a2, 0px 2px 1px #0066a2, 0px -2px 1px #0066a2, 0px 4px 1px #004a87, 2px 4px 1px #004a87, -2px 4px 1px #004a87;
|
||||
border: none;
|
||||
margin: 15px 15px 30px;
|
||||
background: repeating-linear-gradient( 45deg, #3ebbf7, #3ebbf7 5px, #45b1f4 5px, #45b1f4 10px);
|
||||
border-bottom: 3px solid rgba(16, 91, 146, 0.5);
|
||||
border-top: 3px solid rgba(255,255,255,.3);
|
||||
color: #fff !important;
|
||||
border-radius: 8px;
|
||||
padding: 8px 15px 10px;
|
||||
box-shadow: 0 6px 0 #266b91, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #12517d, 0 12px 0 5px #1a6b9a, 0 15px 0 5px #0c405e, 0 15px 1px 6px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.game-button:hover {
|
||||
top:2px;
|
||||
box-shadow: 0 4px 0 #266b91, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #12517d, 0 10px 0 5px #1a6b9a, 0 13px 0 5px #0c405e, 0 13px 1px 6px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.game-button::before {
|
||||
content: '';
|
||||
height: 10%;
|
||||
position: absolute;
|
||||
width: 40%;
|
||||
background: #fff;
|
||||
right: 13%;
|
||||
top: -3%;
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.game-button::after {
|
||||
content: '';
|
||||
height: 10%;
|
||||
position: absolute;
|
||||
width: 5%;
|
||||
background: #fff;
|
||||
right: 5%;
|
||||
top: -3%;
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.game-button.orange {
|
||||
background: repeating-linear-gradient( 45deg, #ffc800, #ffc800 5px, #ffc200 5px, #ffc200 10px);
|
||||
box-shadow: 0 6px 0 #b76113, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #75421f, 0 12px 0 5px #8a542b, 0 15px 0 5px #593116, 0 15px 1px 6px rgba(0,0,0,.3);
|
||||
border-bottom: 3px solid rgba(205, 102, 0, 0.5);
|
||||
text-shadow: 2px 2px 1px #e78700, -2px 2px 1px #e78700, 2px -2px 1px #e78700, -2px -2px 1px #e78700, 0px 2px 1px #e78700, 0px -2px 1px #e78700, 0px 4px 1px #c96100, 2px 4px 1px #c96100, -2px 4px 1px #c96100;
|
||||
}
|
||||
|
||||
.game-button.orange:hover {
|
||||
top:2px;
|
||||
box-shadow: 0 4px 0 #b76113, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #75421f, 0 10px 0 5px #8a542b, 0 13px 0 5px #593116, 0 13px 1px 6px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.game-button.red {
|
||||
background: repeating-linear-gradient( 45deg, #ff4f4c, #ff4f4c 5px, #ff4643 5px, #ff4643 10px);
|
||||
box-shadow: 0 6px 0 #ae2725, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #831614, 0 12px 0 5px #a33634, 0 15px 0 5px #631716, 0 15px 1px 6px rgba(0,0,0,.3);
|
||||
border-bottom: 3px solid rgba(160, 25, 23, 0.5);
|
||||
text-shadow: 2px 2px 1px #d72d21, -2px 2px 1px #d72d21, 2px -2px 1px #d72d21, -2px -2px 1px #d72d21, 0px 2px 1px #d72d21, 0px -2px 1px #d72d21, 0px 4px 1px #930704, 2px 4px 1px #930704, -2px 4px 1px #930704;
|
||||
}
|
||||
|
||||
.game-button.red:hover {
|
||||
top:2px;
|
||||
box-shadow: 0 4px 0 #ae2725, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #831614, 0 10px 0 5px #a33634, 0 13px 0 5px #631716, 0 13px 1px 6px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.game-button.green {
|
||||
background: repeating-linear-gradient( 45deg, #54d440, #54d440 5px, #52cc3f 5px, #52cc3f 10px);
|
||||
box-shadow: 0 6px 0 #348628, 0 8px 1px 1px rgba(0,0,0,.3), 0 10px 0 5px #2a6d20, 0 12px 0 5px #39822e, 0 15px 0 5px #1d4c16, 0 15px 1px 6px rgba(0,0,0,.3);
|
||||
border-bottom: 3px solid rgba(40, 117, 29, 0.5);
|
||||
text-shadow: 2px 2px 1px #348628, -2px 2px 1px #348628, 2px -2px 1px #348628, -2px -2px 1px #348628, 0px 2px 1px #348628, 0px -2px 1px #348628, 0px 4px 1px #1d4c16, 2px 4px 1px #1d4c16, -2px 4px 1px #1d4c16;
|
||||
}
|
||||
|
||||
.game-button.green:hover {
|
||||
top:2px;
|
||||
box-shadow: 0 4px 0 #348628, 0 6px 1px 1px rgba(0,0,0,.3), 0 8px 0 5px #2a6d20, 0 10px 0 5px #39822e, 0 13px 0 5px #1d4c16, 0 13px 1px 6px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
border-radius: 50%;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation: load7 1.8s infinite ease-in-out;
|
||||
animation: load7 1.8s infinite ease-in-out;
|
||||
}
|
||||
.loader {
|
||||
color: #ffffff;
|
||||
font-size: 5px;
|
||||
margin: 0 50px 0 50px;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.loader:before {
|
||||
left: -3.5em;
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.loader:after {
|
||||
left: 3.5em;
|
||||
}
|
||||
@-webkit-keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
@keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chat Box */
|
||||
.chat-box{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
background: white;
|
||||
width: 355px;
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
z-index: 100;
|
||||
}
|
||||
.chat-box-player1{
|
||||
right: 20px;
|
||||
}
|
||||
.chat-box-player2{
|
||||
left: 20px;
|
||||
}
|
||||
.chat-head{
|
||||
width: inherit;
|
||||
height: 45px;
|
||||
background: #2c3e50;
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
}
|
||||
.chat-head h2{
|
||||
color: white;
|
||||
padding-top: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.chat-head span{
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
width: 25px;
|
||||
margin: 10px;
|
||||
}
|
||||
.chat-body{
|
||||
display: none;
|
||||
height: 205px;
|
||||
width: inherit;
|
||||
overflow: hidden auto;
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
.chat-text{
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
height: 45px;
|
||||
width: inherit;
|
||||
}
|
||||
.chat-text input{
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #bdc3c7;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
.chat-text input:active, .chat-text input:focus, .chat-text input:hover{
|
||||
border-color: royalblue;
|
||||
}
|
||||
.msg-send{
|
||||
background: #406a4b;
|
||||
}
|
||||
.msg-receive{
|
||||
background: #595080;
|
||||
}
|
||||
.msg-send, .msg-receive{
|
||||
width: 285px;
|
||||
height: 35px;
|
||||
padding: 5px 5px 5px 10px;
|
||||
margin: 5px auto;
|
||||
border-radius: 3px;
|
||||
line-height: 30px;
|
||||
position: relative;
|
||||
color: white;
|
||||
}
|
||||
.msg-receive:before{
|
||||
content: '';
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border: 15px solid;
|
||||
border-color: transparent #595080 transparent transparent;
|
||||
left: -29px;
|
||||
top: 7px;
|
||||
}
|
||||
.msg-send:after{
|
||||
content: '';
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border: 15px solid;
|
||||
border-color: transparent transparent transparent #406a4b;
|
||||
right: -29px;
|
||||
top: 7px;
|
||||
}
|
||||
.msg-receive:hover, .msg-send:hover{
|
||||
opacity: .9;
|
||||
}
|
||||
17
uno-online/client/src/App.js
Normal file
17
uno-online/client/src/App.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import './App.css'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Homepage from './components/Homepage'
|
||||
import Game from './components/Game'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<div className="App">
|
||||
<Routes>
|
||||
<Route path='/' element={<Homepage />} />
|
||||
<Route path='/play' element={<Game />} />
|
||||
</Routes>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
1595
uno-online/client/src/components/Game.js
Normal file
1595
uno-online/client/src/components/Game.js
Normal file
File diff suppressed because it is too large
Load Diff
27
uno-online/client/src/components/Homepage.js
Normal file
27
uno-online/client/src/components/Homepage.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import randomCodeGenerator from '../utils/randomCodeGenerator'
|
||||
|
||||
const Homepage = () => {
|
||||
const [roomCode, setRoomCode] = useState('')
|
||||
|
||||
return (
|
||||
<div className='Homepage'>
|
||||
<div className='homepage-menu'>
|
||||
<img src={require('../assets/logo.png').default} width='200px' />
|
||||
<div className='homepage-form'>
|
||||
<div className='homepage-join'>
|
||||
<input type='text' placeholder='Game Code' onChange={(event) => setRoomCode(event.target.value)} />
|
||||
<Link to={`/play?roomCode=${roomCode}`}><button className="game-button green">JOIN GAME</button></Link>
|
||||
</div>
|
||||
<h1>OR</h1>
|
||||
<div className='homepage-create'>
|
||||
<Link to={`/play?roomCode=${randomCodeGenerator(5)}`}><button className="game-button orange">CREATE GAME</button></Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Homepage
|
||||
9
uno-online/client/src/components/Spinner.js
Normal file
9
uno-online/client/src/components/Spinner.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const Spinner = () => {
|
||||
return (
|
||||
<div className="loader">Loading...</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
14
uno-online/client/src/index.css
Normal file
14
uno-online/client/src/index.css
Normal file
@@ -0,0 +1,14 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Carter+One');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Carter One', sans-serif;
|
||||
color: white;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
14
uno-online/client/src/index.js
Normal file
14
uno-online/client/src/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
231
uno-online/client/src/utils/botActionExecutor.js
Normal file
231
uno-online/client/src/utils/botActionExecutor.js
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Bot Action Executor
|
||||
* Processes bot actions and executes them in the game
|
||||
*/
|
||||
|
||||
import { isCardPlayable } from './cardParser';
|
||||
|
||||
/**
|
||||
* Execute a bot action in the game
|
||||
* @param {Object} action - The bot action to execute
|
||||
* @param {Object} gameContext - Current game context
|
||||
* @returns {Object} Result of the action execution
|
||||
*/
|
||||
export const executeBotAction = (action, gameContext) => {
|
||||
const {
|
||||
turn,
|
||||
currentUser,
|
||||
currentColor,
|
||||
currentNumber,
|
||||
player2Deck,
|
||||
onCardPlayedHandler,
|
||||
onCardDrawnHandler,
|
||||
setUnoButtonPressed
|
||||
} = gameContext
|
||||
|
||||
// Validate it's Player 2's turn and the current user is Player 2
|
||||
if (turn !== 'Player 2') {
|
||||
console.warn('❌ Bot action rejected: Not Player 2\'s turn')
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not your turn',
|
||||
message: 'It\'s not Player 2\'s turn'
|
||||
}
|
||||
}
|
||||
|
||||
if (currentUser !== 'Player 2') {
|
||||
console.warn('❌ Bot action rejected: Current user is not Player 2')
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not Player 2',
|
||||
message: 'Current user is not Player 2'
|
||||
}
|
||||
}
|
||||
|
||||
// Process the action based on type
|
||||
switch (action.action) {
|
||||
case 'play':
|
||||
return executePlayAction(action, gameContext)
|
||||
|
||||
case 'draw':
|
||||
return executeDrawAction(gameContext)
|
||||
|
||||
case 'uno':
|
||||
return executeUnoAction(gameContext)
|
||||
|
||||
default:
|
||||
console.warn('❌ Unknown bot action:', action.action)
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unknown action',
|
||||
message: `Unknown action type: ${action.action}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a play card action
|
||||
*/
|
||||
const executePlayAction = (action, gameContext) => {
|
||||
const { card, color, callUno } = action
|
||||
const { player2Deck, onCardPlayedHandler, setUnoButtonPressed } = gameContext
|
||||
|
||||
// Validate card parameter
|
||||
if (!card) {
|
||||
console.warn('❌ Bot play action rejected: No card specified')
|
||||
return {
|
||||
success: false,
|
||||
error: 'No card specified',
|
||||
message: 'Play action requires a card parameter'
|
||||
}
|
||||
}
|
||||
|
||||
// Check if card is in Player 2's hand
|
||||
if (!player2Deck.includes(card)) {
|
||||
console.warn('❌ Bot play action rejected: Card not in hand:', card)
|
||||
return {
|
||||
success: false,
|
||||
error: 'Card not in hand',
|
||||
message: `Card ${card} is not in Player 2's hand`
|
||||
}
|
||||
}
|
||||
|
||||
// Validate wild card has color specified
|
||||
if ((card === 'W' || card === 'D4W') && !color) {
|
||||
console.warn('❌ Bot play action rejected: Wild card without color')
|
||||
return {
|
||||
success: false,
|
||||
error: 'No color specified',
|
||||
message: 'Wild cards require a color parameter (R/G/B/Y)'
|
||||
}
|
||||
}
|
||||
|
||||
// Handle UNO call if specified
|
||||
if (callUno) {
|
||||
console.log('🔥 Bot called UNO!')
|
||||
setUnoButtonPressed(true)
|
||||
}
|
||||
|
||||
// Store color for wild cards in a way the game can access it
|
||||
if (color && (card === 'W' || card === 'D4W')) {
|
||||
// Set a global or context variable for the color choice
|
||||
// The onCardPlayedHandler will check this
|
||||
window.botChosenColor = color
|
||||
console.log(`🌈 Bot chose color: ${color}`)
|
||||
}
|
||||
|
||||
// Execute the play
|
||||
console.log(`🎴 Bot playing card: ${card}`)
|
||||
console.log(`🎴 Current game state:`, {
|
||||
turn: gameContext.turn,
|
||||
currentUser: gameContext.currentUser,
|
||||
player2DeckLength: gameContext.player2Deck?.length,
|
||||
hasCard: gameContext.player2Deck?.includes(card)
|
||||
})
|
||||
|
||||
// Track deck size before play to verify card was actually played
|
||||
const deckSizeBefore = gameContext.player2Deck.length
|
||||
|
||||
onCardPlayedHandler(card)
|
||||
|
||||
// Wait a moment for state to update
|
||||
// Note: This is a hack because onCardPlayedHandler doesn't return success/failure
|
||||
// In a real implementation, we'd need to refactor the game logic
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const deckSizeAfter = gameContext.player2Deck.length
|
||||
const cardWasPlayed = deckSizeAfter < deckSizeBefore
|
||||
|
||||
if (cardWasPlayed) {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Played card ${card}${color ? ` (chose ${color})` : ''}${callUno ? ' and called UNO' : ''}`
|
||||
})
|
||||
} else {
|
||||
console.warn('❌ Card was not actually played (deck size unchanged)')
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Invalid play',
|
||||
message: `Card ${card} could not be played (invalid move)`
|
||||
})
|
||||
}
|
||||
}, 100) // Wait 100ms for state update
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a draw card action
|
||||
*/
|
||||
const executeDrawAction = (gameContext) => {
|
||||
const { onCardDrawnHandler } = gameContext
|
||||
|
||||
console.log('📥 Bot drawing a card')
|
||||
onCardDrawnHandler()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Drew a card'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a UNO call action
|
||||
*/
|
||||
const executeUnoAction = (gameContext) => {
|
||||
const { setUnoButtonPressed, player2Deck } = gameContext
|
||||
|
||||
if (player2Deck.length !== 2) {
|
||||
console.warn('❌ Bot UNO rejected: Player 2 doesn\'t have exactly 2 cards')
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid UNO call',
|
||||
message: `Can only call UNO with 2 cards, currently have ${player2Deck.length}`
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔥 Bot called UNO!')
|
||||
setUnoButtonPressed(true)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Called UNO'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a bot action before execution
|
||||
*/
|
||||
export const validateBotAction = (action, gameState) => {
|
||||
if (!action || typeof action !== 'object') {
|
||||
return { valid: false, error: 'Invalid action format' }
|
||||
}
|
||||
|
||||
if (!action.action) {
|
||||
return { valid: false, error: 'Missing action type' }
|
||||
}
|
||||
|
||||
// Validate specific action types
|
||||
switch (action.action) {
|
||||
case 'play':
|
||||
if (!action.card) {
|
||||
return { valid: false, error: 'Play action requires card parameter' }
|
||||
}
|
||||
if ((action.card === 'W' || action.card === 'D4W') && !action.color) {
|
||||
return { valid: false, error: 'Wild cards require color parameter' }
|
||||
}
|
||||
if (action.color && !['R', 'G', 'B', 'Y'].includes(action.color)) {
|
||||
return { valid: false, error: 'Invalid color. Must be R, G, B, or Y' }
|
||||
}
|
||||
break
|
||||
|
||||
case 'draw':
|
||||
case 'uno':
|
||||
// No additional validation needed
|
||||
break
|
||||
|
||||
default:
|
||||
return { valid: false, error: `Unknown action type: ${action.action}` }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
26
uno-online/client/src/utils/cardAssets.js
Normal file
26
uno-online/client/src/utils/cardAssets.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Build a map of all card front images using webpack's require.context
|
||||
import CARD_BACK from '../assets/card-back.png'
|
||||
|
||||
// require all png files in cards-front directory
|
||||
const req = require.context('../assets/cards-front', false, /\.png$/)
|
||||
|
||||
const cardMap = {}
|
||||
req.keys().forEach((key) => {
|
||||
// key is like './5R.png' -> strip './' and '.png'
|
||||
const code = key.replace('./', '').replace('.png', '')
|
||||
try {
|
||||
const resolved = req(key)
|
||||
cardMap[code] = resolved && resolved.default ? resolved.default : resolved
|
||||
} catch (e) {
|
||||
cardMap[code] = CARD_BACK
|
||||
}
|
||||
})
|
||||
|
||||
export const getCardImage = (code) => {
|
||||
if (!code) return CARD_BACK
|
||||
return cardMap[code] || CARD_BACK
|
||||
}
|
||||
|
||||
export default {
|
||||
getCardImage,
|
||||
}
|
||||
150
uno-online/client/src/utils/cardParser.js
Normal file
150
uno-online/client/src/utils/cardParser.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Utility functions for parsing UNO card codes into readable JSON objects
|
||||
* Card codes format: '5R', 'D2G', 'skipB', 'W', 'D4W', '_Y' (reverse)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a card code into a detailed card object
|
||||
* @param {string} cardCode - The card code (e.g., '5R', 'D2G', 'W')
|
||||
* @returns {object} Card object with type, value, color, and display name
|
||||
*/
|
||||
export const parseCard = (cardCode) => {
|
||||
if (!cardCode) return null;
|
||||
|
||||
const card = {
|
||||
code: cardCode,
|
||||
type: 'unknown',
|
||||
value: null,
|
||||
color: null,
|
||||
colorName: null,
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
// Extract color (last character for most cards)
|
||||
const lastChar = cardCode.charAt(cardCode.length - 1).toUpperCase();
|
||||
const colorMap = {
|
||||
'R': 'red',
|
||||
'G': 'green',
|
||||
'B': 'blue',
|
||||
'Y': 'yellow'
|
||||
};
|
||||
|
||||
// Wild cards (no color)
|
||||
if (cardCode === 'W') {
|
||||
card.type = 'wild';
|
||||
card.value = 300;
|
||||
card.displayName = 'Wild';
|
||||
return card;
|
||||
}
|
||||
|
||||
if (cardCode === 'D4W') {
|
||||
card.type = 'draw4_wild';
|
||||
card.value = 600;
|
||||
card.displayName = 'Draw 4 Wild';
|
||||
return card;
|
||||
}
|
||||
|
||||
// Cards with color
|
||||
card.color = lastChar;
|
||||
card.colorName = colorMap[lastChar] || 'unknown';
|
||||
|
||||
// Number cards (0-9)
|
||||
const firstChar = cardCode.charAt(0);
|
||||
if (firstChar >= '0' && firstChar <= '9') {
|
||||
card.type = 'number';
|
||||
card.value = parseInt(firstChar, 10);
|
||||
card.displayName = `${firstChar} ${card.colorName}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
// Skip cards
|
||||
if (cardCode.startsWith('skip')) {
|
||||
card.type = 'skip';
|
||||
card.value = 404;
|
||||
card.displayName = `Skip ${card.colorName}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
// Draw 2 cards
|
||||
if (cardCode.startsWith('D2')) {
|
||||
card.type = 'draw2';
|
||||
card.value = 252;
|
||||
card.displayName = `Draw 2 ${card.colorName}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
// Reverse cards
|
||||
if (cardCode === '_' + lastChar) {
|
||||
card.type = 'reverse';
|
||||
card.value = 0;
|
||||
card.displayName = `Reverse ${card.colorName}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
return card;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a card can be played on the current card
|
||||
* @param {string} cardCode - The card to check
|
||||
* @param {string} currentColor - Current color in play
|
||||
* @param {string|number} currentNumber - Current number/value in play
|
||||
* @returns {boolean} Whether the card can be played
|
||||
*/
|
||||
export const isCardPlayable = (cardCode, currentColor, currentNumber) => {
|
||||
const card = parseCard(cardCode);
|
||||
|
||||
// Wild cards can always be played
|
||||
if (card.type === 'wild' || card.type === 'draw4_wild') {
|
||||
return true;
|
||||
}
|
||||
// Normalize currentColor: accept 'R' or 'red' (case-insensitive)
|
||||
let normColor = null;
|
||||
if (typeof currentColor === 'string') {
|
||||
const c = currentColor.trim().toUpperCase();
|
||||
const nameToLetter = { RED: 'R', GREEN: 'G', BLUE: 'B', YELLOW: 'Y' };
|
||||
normColor = nameToLetter[c] || c.charAt(0);
|
||||
}
|
||||
|
||||
// Check color match
|
||||
if (card.color && normColor && card.color === normColor) {
|
||||
return true;
|
||||
}
|
||||
// Check number/value match (coerce currentNumber to number)
|
||||
const normNumber = (currentNumber === null || currentNumber === undefined) ? null : Number(currentNumber);
|
||||
if (card.value !== null && normNumber !== null && card.value === normNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Special case: reverse cards
|
||||
if (card.type === 'reverse' && normNumber === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse multiple cards into detailed objects
|
||||
* @param {string[]} cardCodes - Array of card codes
|
||||
* @returns {object[]} Array of parsed card objects
|
||||
*/
|
||||
export const parseCards = (cardCodes) => {
|
||||
return cardCodes.map(parseCard);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get playable cards from a hand
|
||||
* @param {string[]} hand - Player's card codes
|
||||
* @param {string} currentColor - Current color in play
|
||||
* @param {string|number} currentNumber - Current number/value in play
|
||||
* @returns {object[]} Array of playable cards with their details
|
||||
*/
|
||||
export const getPlayableCards = (hand, currentColor, currentNumber) => {
|
||||
return hand
|
||||
.filter(cardCode => isCardPlayable(cardCode, currentColor, currentNumber))
|
||||
.map(cardCode => ({
|
||||
...parseCard(cardCode),
|
||||
isPlayable: true
|
||||
}));
|
||||
};
|
||||
167
uno-online/client/src/utils/gameStateBuilder.js
Normal file
167
uno-online/client/src/utils/gameStateBuilder.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Utility for building comprehensive game state JSON for bot integration
|
||||
*/
|
||||
|
||||
import { parseCard, parseCards, getPlayableCards, isCardPlayable } from './cardParser';
|
||||
|
||||
/**
|
||||
* Build a complete game state object for external consumption (e.g., bot/AI)
|
||||
* @param {object} gameState - Current game state from React component
|
||||
* @param {string} currentUser - The current user's player name (Player 1 or Player 2)
|
||||
* @returns {object} Comprehensive game state in JSON format
|
||||
*/
|
||||
export const buildGameStateJSON = (gameState, currentUser) => {
|
||||
const {
|
||||
gameOver,
|
||||
winner,
|
||||
turn,
|
||||
player1Deck,
|
||||
player2Deck,
|
||||
currentColor,
|
||||
currentNumber,
|
||||
playedCardsPile,
|
||||
drawCardPile
|
||||
} = gameState;
|
||||
|
||||
// Get last 5 played cards (or all if less than 5)
|
||||
const recentCards = playedCardsPile.slice(-5);
|
||||
const currentCard = playedCardsPile[playedCardsPile.length - 1];
|
||||
|
||||
// Determine which player is the bot (Player 2)
|
||||
const botDeck = player2Deck;
|
||||
const opponentDeck = player1Deck;
|
||||
const isBotTurn = turn === 'Player 2';
|
||||
|
||||
// Parse bot's cards with playability info
|
||||
// Parse bot's cards and mark playability based on game rules (independent of whose turn)
|
||||
const botParsedCards = botDeck.map(cardCode => {
|
||||
const card = parseCard(cardCode);
|
||||
const playable = isCardPlayable(cardCode, currentColor, currentNumber);
|
||||
return {
|
||||
...card,
|
||||
isPlayable: playable
|
||||
};
|
||||
});
|
||||
|
||||
// Build the comprehensive state object
|
||||
return {
|
||||
// Game meta info
|
||||
game: {
|
||||
isOver: gameOver,
|
||||
winner: winner || null,
|
||||
currentTurn: turn,
|
||||
turnNumber: playedCardsPile.length, // Approximate turn count
|
||||
},
|
||||
|
||||
// Current card on the pile
|
||||
currentCard: {
|
||||
code: currentCard,
|
||||
...parseCard(currentCard),
|
||||
currentColor: currentColor,
|
||||
currentNumber: currentNumber
|
||||
},
|
||||
|
||||
// Recently played cards (last 5)
|
||||
recentlyPlayed: recentCards.map((cardCode, index) => ({
|
||||
code: cardCode,
|
||||
...parseCard(cardCode),
|
||||
position: recentCards.length - index // 1 = most recent
|
||||
})),
|
||||
|
||||
// Player 1 info (opponent to bot)
|
||||
player1: {
|
||||
name: 'Player 1',
|
||||
cardCount: player1Deck.length,
|
||||
isCurrentTurn: turn === 'Player 1',
|
||||
cards: [] // Hidden from bot
|
||||
},
|
||||
|
||||
// Player 2 info (bot)
|
||||
player2: {
|
||||
name: 'Player 2',
|
||||
cardCount: player2Deck.length,
|
||||
isCurrentTurn: turn === 'Player 2',
|
||||
cards: botParsedCards, // Visible to bot
|
||||
playableCards: botParsedCards.filter(c => c.isPlayable)
|
||||
},
|
||||
|
||||
// Deck info
|
||||
deck: {
|
||||
drawPileCount: drawCardPile.length,
|
||||
playedPileCount: playedCardsPile.length
|
||||
},
|
||||
|
||||
// Bot decision context
|
||||
botContext: {
|
||||
canPlay: isBotTurn && botParsedCards.some(c => c.isPlayable),
|
||||
mustDraw: isBotTurn && !botParsedCards.some(c => c.isPlayable),
|
||||
hasUno: player2Deck.length === 2, // Should press UNO button next turn
|
||||
isWinning: player2Deck.length === 1,
|
||||
actions: isBotTurn ? getAvailableActions(botParsedCards, currentColor, currentNumber) : []
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get available actions for the bot
|
||||
* @param {object[]} parsedCards - Bot's parsed cards
|
||||
* @param {string} currentColor - Current color
|
||||
* @param {number|string} currentNumber - Current number
|
||||
* @returns {object[]} Array of available action objects
|
||||
*/
|
||||
const getAvailableActions = (parsedCards, currentColor, currentNumber) => {
|
||||
const actions = [];
|
||||
|
||||
// Check each card for playability
|
||||
parsedCards.forEach(card => {
|
||||
if (card.isPlayable) {
|
||||
actions.push({
|
||||
action: 'play_card',
|
||||
card: {
|
||||
code: card.code,
|
||||
type: card.type,
|
||||
value: card.value,
|
||||
color: card.color,
|
||||
displayName: card.displayName
|
||||
},
|
||||
// For wild cards, need to choose a color
|
||||
requiresColorChoice: card.type === 'wild' || card.type === 'draw4_wild'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no playable cards, must draw
|
||||
if (actions.length === 0) {
|
||||
actions.push({
|
||||
action: 'draw_card',
|
||||
card: null
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format game state for console logging
|
||||
* @param {object} gameStateJSON - The game state object
|
||||
* @returns {string} Formatted JSON string
|
||||
*/
|
||||
export const formatGameStateForLog = (gameStateJSON) => {
|
||||
return JSON.stringify(gameStateJSON, null, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a simplified game state for quick display
|
||||
* @param {object} gameStateJSON - The full game state object
|
||||
* @returns {object} Simplified state
|
||||
*/
|
||||
export const simplifyGameState = (gameStateJSON) => {
|
||||
return {
|
||||
turn: gameStateJSON.game.currentTurn,
|
||||
currentCard: `${gameStateJSON.currentCard.displayName} (${gameStateJSON.currentCard.code})`,
|
||||
player1Cards: gameStateJSON.player1.cardCount,
|
||||
player2Cards: gameStateJSON.player2.cardCount,
|
||||
botCanPlay: gameStateJSON.botContext.canPlay,
|
||||
playableCards: gameStateJSON.player2.playableCards.length
|
||||
};
|
||||
};
|
||||
8
uno-online/client/src/utils/packOfCards.js
Normal file
8
uno-online/client/src/utils/packOfCards.js
Normal file
@@ -0,0 +1,8 @@
|
||||
//pack of 108 cards (_ = reverse)
|
||||
export default [
|
||||
'0R', '1R', '1R', '2R', '2R', '3R', '3R', '4R', '4R', '5R', '5R', '6R', '6R', '7R', '7R', '8R', '8R', '9R', '9R', 'skipR', 'skipR', '_R', '_R', 'D2R', 'D2R',
|
||||
'0G', '1G', '1G', '2G', '2G', '3G', '3G', '4G', '4G', '5G', '5G', '6G', '6G', '7G', '7G', '8G', '8G', '9G', '9G', 'skipG', 'skipG', '_G', '_G', 'D2G', 'D2G',
|
||||
'0B', '1B', '1B', '2B', '2B', '3B', '3B', '4B', '4B', '5B', '5B', '6B', '6B', '7B', '7B', '8B', '8B', '9B', '9B', 'skipB', 'skipB', '_B', '_B', 'D2B', 'D2B',
|
||||
'0Y', '1Y', '1Y', '2Y', '2Y', '3Y', '3Y', '4Y', '4Y', '5Y', '5Y', '6Y', '6Y', '7Y', '7Y', '8Y', '8Y', '9Y', '9Y', 'skipY', 'skipY', '_Y', '_Y', 'D2Y', 'D2Y',
|
||||
'W', 'W', 'W', 'W', 'D4W', 'D4W', 'D4W', 'D4W'
|
||||
]
|
||||
9
uno-online/client/src/utils/randomCodeGenerator.js
Normal file
9
uno-online/client/src/utils/randomCodeGenerator.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function makeid(length) {
|
||||
var result = '';
|
||||
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var charactersLength = characters.length;
|
||||
for ( var i = 0; i < length; i++ ) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
9
uno-online/client/src/utils/shuffleArray.js
Normal file
9
uno-online/client/src/utils/shuffleArray.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function shuffleArray(array) {
|
||||
for (var i = array.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1))
|
||||
var temp = array[i]
|
||||
array[i] = array[j]
|
||||
array[j] = temp;
|
||||
}
|
||||
return array
|
||||
}
|
||||
Reference in New Issue
Block a user