added handling for end of game
This commit is contained in:
parent
97513eac2d
commit
80caed1498
39
README.md
39
README.md
@ -1,29 +1,33 @@
|
|||||||
# <center>Online Multiplayer Ludo Game</center>
|
# <center>Online Multiplayer Ludo Game</center>
|
||||||
|
|
||||||
|
\>\> [Play Online here](www.github.com/wenszel/mern-ludo) <<
|
||||||
|
|
||||||
\>\> [Play Online here](www.github.com/wenszel/mern-ludo) <<
|
\>\> [Watch YouTube Video here](www.github.com/wenszel/mern-ludo) <<
|
||||||
|
|
||||||
\>\> [Watch YouTube Video here](www.github.com/wenszel/mern-ludo) <<
|
## Table of content
|
||||||
|
|
||||||
## Table of content
|
- [About](#about)
|
||||||
|
|
||||||
- [About](#about)
|
- [Architecture](#architecture)
|
||||||
|
|
||||||
- [Architecture](#architecture)
|
- [Key Features and Challenges](#key-features-and-challenges)
|
||||||
|
|
||||||
- [Key Features and Challenges](#key-features-and-challenges)
|
- [Tech Stack](#tech-stack)
|
||||||
|
|
||||||
- [Tech Stack](#tech-stack)
|
- [Installation](#installation)
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Screenshots](#screenshots)
|
||||||
|
|
||||||
- [Screenshots](#screenshots)
|
## About
|
||||||
|
|
||||||
## About
|
|
||||||
Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication.
|
Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||

|

|
||||||
## Tech Stack
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
Frontend:
|
Frontend:
|
||||||
  
|
  
|
||||||
  
|
  
|
||||||
@ -34,7 +38,7 @@ Tests:
|
|||||||
Tools:
|
Tools:
|
||||||
    
|
    
|
||||||
|
|
||||||
## Key Features and Challenges
|
## Key Features and Challenges
|
||||||
|
|
||||||
- Maintained session consistency with **Express Session** and **MongoDB**.
|
- Maintained session consistency with **Express Session** and **MongoDB**.
|
||||||
- Enabled real-time communication via **WebSocket** and **SocketIO**.
|
- Enabled real-time communication via **WebSocket** and **SocketIO**.
|
||||||
@ -42,14 +46,15 @@ Tools:
|
|||||||
- Hosted in a **Docker** container on **AWS EC2**.
|
- Hosted in a **Docker** container on **AWS EC2**.
|
||||||
- Established CI/CD using **CircleCI**.
|
- Established CI/CD using **CircleCI**.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
## Installation
|
|
||||||
1. Download this repository
|
1. Download this repository
|
||||||
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
|
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority
|
mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Add this URL to the /backend/credentials.js file
|
3. Add this URL to the /backend/credentials.js file
|
||||||
4. Perform these commands in the main directory:
|
4. Perform these commands in the main directory:
|
||||||
|
|
||||||
@ -61,6 +66,10 @@ npm i
|
|||||||
node server.js
|
node server.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const { getRoom, updateRoom } = require('../services/roomService');
|
const { getRoom, updateRoom } = require('../services/roomService');
|
||||||
const { sendToPlayersRolledNumber } = require('../socket/emits');
|
const { sendToPlayersRolledNumber, sendWinner } = require('../socket/emits');
|
||||||
const { rollDice, isMoveValid } = require('./handlersFunctions');
|
const { rollDice, isMoveValid } = require('./handlersFunctions');
|
||||||
|
|
||||||
module.exports = socket => {
|
module.exports = socket => {
|
||||||
@ -7,12 +7,18 @@ module.exports = socket => {
|
|||||||
|
|
||||||
const handleMovePawn = async pawnId => {
|
const handleMovePawn = async pawnId => {
|
||||||
const room = await getRoom(req.session.roomId);
|
const room = await getRoom(req.session.roomId);
|
||||||
|
if (room.winner) return;
|
||||||
const pawn = room.getPawn(pawnId);
|
const pawn = room.getPawn(pawnId);
|
||||||
if (isMoveValid(req.session, pawn, room)) {
|
if (isMoveValid(req.session, pawn, room)) {
|
||||||
const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
|
const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
|
||||||
room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
||||||
room.beatPawns(newPositionOfMovedPawn, req.session.color);
|
room.beatPawns(newPositionOfMovedPawn, req.session.color);
|
||||||
room.changeMovingPlayer();
|
room.changeMovingPlayer();
|
||||||
|
const winner = room.getWinner();
|
||||||
|
if (winner) {
|
||||||
|
room.endGame(winner);
|
||||||
|
sendWinner(room._id.toString(), winner);
|
||||||
|
}
|
||||||
await updateRoom(room);
|
await updateRoom(room);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const { sendToPlayersRolledNumber } = require('../socket/emits');
|
const { sendToPlayersRolledNumber, sendWinner } = require('../socket/emits');
|
||||||
|
|
||||||
const rollDice = () => {
|
const rollDice = () => {
|
||||||
const rolledNumber = Math.ceil(Math.random() * 6);
|
const rolledNumber = Math.ceil(Math.random() * 6);
|
||||||
@ -8,6 +8,7 @@ const rollDice = () => {
|
|||||||
const makeRandomMove = async roomId => {
|
const makeRandomMove = async roomId => {
|
||||||
const { updateRoom, getRoom } = require('../services/roomService');
|
const { updateRoom, getRoom } = require('../services/roomService');
|
||||||
const room = await getRoom(roomId);
|
const room = await getRoom(roomId);
|
||||||
|
if (room.winner) return;
|
||||||
if (room.rolledNumber === null) {
|
if (room.rolledNumber === null) {
|
||||||
room.rolledNumber = rollDice();
|
room.rolledNumber = rollDice();
|
||||||
sendToPlayersRolledNumber(room._id.toString(), room.rolledNumber);
|
sendToPlayersRolledNumber(room._id.toString(), room.rolledNumber);
|
||||||
@ -19,6 +20,11 @@ const makeRandomMove = async roomId => {
|
|||||||
room.movePawn(randomPawn);
|
room.movePawn(randomPawn);
|
||||||
}
|
}
|
||||||
room.changeMovingPlayer();
|
room.changeMovingPlayer();
|
||||||
|
const winner = room.getWinner();
|
||||||
|
if (winner) {
|
||||||
|
room.endGame(winner);
|
||||||
|
sendWinner(room._id.toString(), winner);
|
||||||
|
}
|
||||||
await updateRoom(room);
|
await updateRoom(room);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,14 @@ module.exports = socket => {
|
|||||||
addPlayerToExistingRoom(room, data);
|
addPlayerToExistingRoom(room, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExit = async () => {
|
||||||
|
req.session.reload(err => {
|
||||||
|
if (err) return socket.disconnect();
|
||||||
|
req.session.destroy();
|
||||||
|
socket.emit('redirect');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleReady = async () => {
|
const handleReady = async () => {
|
||||||
const room = await getRoom(req.session.roomId);
|
const room = await getRoom(req.session.roomId);
|
||||||
room.getPlayer(req.session.playerId).changeReadyStatus();
|
room.getPlayer(req.session.playerId).changeReadyStatus();
|
||||||
@ -45,4 +53,5 @@ module.exports = socket => {
|
|||||||
|
|
||||||
socket.on('player:login', handleLogin);
|
socket.on('player:login', handleLogin);
|
||||||
socket.on('player:ready', handleReady);
|
socket.on('player:ready', handleReady);
|
||||||
|
socket.on('player:exit', handleExit);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
|
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
|
||||||
const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits');
|
const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits');
|
||||||
|
|
||||||
module.exports = socket => {
|
module.exports = socket => {
|
||||||
const req = socket.request;
|
const req = socket.request;
|
||||||
@ -13,6 +13,7 @@ module.exports = socket => {
|
|||||||
await updateRoom(room);
|
await updateRoom(room);
|
||||||
}
|
}
|
||||||
sendToOnePlayerData(socket.id, room);
|
sendToOnePlayerData(socket.id, room);
|
||||||
|
if (room.winner) sendWinner(socket.id, room.winner);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGetAllRooms = async () => {
|
const handleGetAllRooms = async () => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ const RoomSchema = new mongoose.Schema({
|
|||||||
timeoutID: Number,
|
timeoutID: Number,
|
||||||
rolledNumber: Number,
|
rolledNumber: Number,
|
||||||
players: [PlayerSchema],
|
players: [PlayerSchema],
|
||||||
|
winner: { type: String, default: null },
|
||||||
pawns: {
|
pawns: {
|
||||||
type: [PawnSchema],
|
type: [PawnSchema],
|
||||||
default: () => {
|
default: () => {
|
||||||
@ -45,6 +46,7 @@ RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.changeMovingPlayer = function () {
|
RoomSchema.methods.changeMovingPlayer = function () {
|
||||||
|
if (this.winner) return;
|
||||||
const playerIndex = this.players.findIndex(player => player.nowMoving === true);
|
const playerIndex = this.players.findIndex(player => player.nowMoving === true);
|
||||||
this.players[playerIndex].nowMoving = false;
|
this.players[playerIndex].nowMoving = false;
|
||||||
if (playerIndex + 1 === this.players.length) {
|
if (playerIndex + 1 === this.players.length) {
|
||||||
@ -87,6 +89,31 @@ RoomSchema.methods.startGame = function () {
|
|||||||
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
|
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.endGame = function (winner) {
|
||||||
|
this.timeoutID = null;
|
||||||
|
this.rolledNumber = null;
|
||||||
|
this.nextMoveTime = null;
|
||||||
|
this.players.map(player => (player.nowMoving = false));
|
||||||
|
this.winner = winner;
|
||||||
|
this.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.getWinner = function () {
|
||||||
|
if (this.pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) {
|
||||||
|
return 'red';
|
||||||
|
}
|
||||||
|
if (this.pawns.filter(pawn => pawn.color === 'blue' && pawn.position === 79).length === 4) {
|
||||||
|
return 'blue';
|
||||||
|
}
|
||||||
|
if (this.pawns.filter(pawn => pawn.color === 'green' && pawn.position === 85).length === 4) {
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
if (this.pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) {
|
||||||
|
return 'yellow';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
RoomSchema.methods.isFull = function () {
|
RoomSchema.methods.isFull = function () {
|
||||||
if (this.players.length === 4) {
|
if (this.players.length === 4) {
|
||||||
this.full = true;
|
this.full = true;
|
||||||
|
|||||||
@ -16,4 +16,14 @@ const sendToOnePlayerRooms = (id, rooms) => {
|
|||||||
socketManager.getIO().to(id).emit('room:rooms', JSON.stringify(rooms));
|
socketManager.getIO().to(id).emit('room:rooms', JSON.stringify(rooms));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { sendToPlayersData, sendToPlayersRolledNumber, sendToOnePlayerData, sendToOnePlayerRooms };
|
const sendWinner = (id, winner) => {
|
||||||
|
socketManager.getIO().to(id).emit('game:winner', winner);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendToPlayersData,
|
||||||
|
sendToPlayersRolledNumber,
|
||||||
|
sendToOnePlayerData,
|
||||||
|
sendToOnePlayerRooms,
|
||||||
|
sendWinner,
|
||||||
|
};
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import { PlayerDataContext, SocketContext } from '../../App';
|
|||||||
import useSocketData from '../../hooks/useSocketData';
|
import useSocketData from '../../hooks/useSocketData';
|
||||||
import Map from './Map/Map';
|
import Map from './Map/Map';
|
||||||
import Navbar from '../Navbar/Navbar';
|
import Navbar from '../Navbar/Navbar';
|
||||||
|
import Overlay from '../Overlay/Overlay';
|
||||||
|
import styles from './Gameboard.module.css';
|
||||||
|
import trophyImage from '../../images/trophy.webp';
|
||||||
|
|
||||||
const Gameboard = () => {
|
const Gameboard = () => {
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
const context = useContext(PlayerDataContext);
|
const context = useContext(PlayerDataContext);
|
||||||
|
|
||||||
const [pawns, setPawns] = useState([]);
|
const [pawns, setPawns] = useState([]);
|
||||||
const [players, setPlayers] = useState([]);
|
const [players, setPlayers] = useState([]);
|
||||||
|
|
||||||
@ -20,6 +22,8 @@ const Gameboard = () => {
|
|||||||
|
|
||||||
const [movingPlayer, setMovingPlayer] = useState('red');
|
const [movingPlayer, setMovingPlayer] = useState('red');
|
||||||
|
|
||||||
|
const [winner, setWinner] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.emit('room:data', context.roomId);
|
socket.emit('room:data', context.roomId);
|
||||||
socket.on('room:data', data => {
|
socket.on('room:data', data => {
|
||||||
@ -47,11 +51,18 @@ const Gameboard = () => {
|
|||||||
setTime(data.nextMoveTime);
|
setTime(data.nextMoveTime);
|
||||||
setStarted(data.started);
|
setStarted(data.started);
|
||||||
});
|
});
|
||||||
}, [socket]);
|
|
||||||
|
socket.on('game:winner', winner => {
|
||||||
|
setWinner(winner);
|
||||||
|
});
|
||||||
|
socket.on('redirect', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}, [socket, context.playerId, context.roomId, setRolledNumber]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(players[0] && !started) || (time && started) ? (
|
{pawns.length === 16 ? (
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<Navbar
|
<Navbar
|
||||||
players={players}
|
players={players}
|
||||||
@ -61,12 +72,24 @@ const Gameboard = () => {
|
|||||||
movingPlayer={movingPlayer}
|
movingPlayer={movingPlayer}
|
||||||
rolledNumber={rolledNumber}
|
rolledNumber={rolledNumber}
|
||||||
nowMoving={nowMoving}
|
nowMoving={nowMoving}
|
||||||
|
ended={winner !== null}
|
||||||
/>
|
/>
|
||||||
<Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} />
|
<Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ReactLoading type='spinningBubbles' color='white' height={667} width={375} />
|
<ReactLoading type='spinningBubbles' color='white' height={667} width={375} />
|
||||||
)}
|
)}
|
||||||
|
{winner ? (
|
||||||
|
<Overlay>
|
||||||
|
<div className={styles.winnerContainer}>
|
||||||
|
<img src={trophyImage} alt='winner' />
|
||||||
|
<h1>
|
||||||
|
1st: <span style={{ color: winner }}>{winner}</span>
|
||||||
|
</h1>
|
||||||
|
<button onClick={() => socket.emit('player:exit')}>Play again</button>
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/components/Gameboard/Gameboard.module.css
Normal file
20
src/components/Gameboard/Gameboard.module.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.winnerContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 40px;
|
||||||
|
width: 300px;
|
||||||
|
height: 250px;
|
||||||
|
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px;
|
||||||
|
z-index: 2;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.winnerContainer > button {
|
||||||
|
align-self: center;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
@ -14,17 +14,19 @@ const ServerListTable = ({ rooms, handleJoinClick }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rooms.map((room, index) => (
|
{rooms.map((room, index) => {
|
||||||
<tr key={index}>
|
return room.started ? null : (
|
||||||
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
<tr key={index}>
|
||||||
<td className={styles.roomName}>{room.name}</td>
|
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||||
<td>{`${room.players.length}/4`}</td>
|
<td className={styles.roomName}>{room.name}</td>
|
||||||
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
<td>{`${room.players.length}/4`}</td>
|
||||||
<td className={styles.lastColumn}>
|
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
||||||
<button onClick={() => handleJoinClick(room)}>Join</button>
|
<td className={styles.lastColumn}>
|
||||||
</td>
|
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||||
</tr>
|
</td>
|
||||||
))}
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useContext } from 'react';
|
|||||||
import { PlayerDataContext } from '../../App';
|
import { PlayerDataContext } from '../../App';
|
||||||
import styles from './Navbar.module.css';
|
import styles from './Navbar.module.css';
|
||||||
|
|
||||||
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, movingPlayer }) => {
|
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, movingPlayer, ended }) => {
|
||||||
const context = useContext(PlayerDataContext);
|
const context = useContext(PlayerDataContext);
|
||||||
|
|
||||||
const diceProps = {
|
const diceProps = {
|
||||||
@ -21,7 +21,7 @@ const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, movi
|
|||||||
{players.map((player, index) => (
|
{players.map((player, index) => (
|
||||||
<div className={`${styles.playerContainer} ${styles[PLAYER_COLORS[index]]}`} key={index}>
|
<div className={`${styles.playerContainer} ${styles[PLAYER_COLORS[index]]}`} key={index}>
|
||||||
<NameContainer player={player} time={time} />
|
<NameContainer player={player} time={time} />
|
||||||
{started ? <Dice playerColor={PLAYER_COLORS[index]} {...diceProps} /> : null}
|
{started && !ended ? <Dice playerColor={PLAYER_COLORS[index]} {...diceProps} /> : null}
|
||||||
{context.color === player.color && !started ? <ReadyButton isReady={isReady} /> : null}
|
{context.color === player.color && !started ? <ReadyButton isReady={isReady} /> : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
BIN
src/images/lobby.png
Normal file
BIN
src/images/lobby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
BIN
src/images/trophy.webp
Normal file
BIN
src/images/trophy.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
src/images/winner.png
Normal file
BIN
src/images/winner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
Loading…
Reference in New Issue
Block a user