diff --git a/README.md b/README.md
index f547e85..2132a4b 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,33 @@
-#
Online Multiplayer Ludo Game
+# Online Multiplayer Ludo Game
+\>\> [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.
+
## Architecture
+

-## Tech Stack
+
+## Tech Stack
+
Frontend:
  
  
@@ -34,22 +38,23 @@ Tests:
Tools:
    
-## Key Features and Challenges
+## Key Features and Challenges
- Maintained session consistency with **Express Session** and **MongoDB**.
- Enabled real-time communication via **WebSocket** and **SocketIO**.
-- Ensured code reliability with testing using **Mocha**, **Chai**, and **Jest**.
+- Ensured code reliability with testing using **Mocha**, **Chai**, and **Jest**.
- Hosted in a **Docker** container on **AWS EC2**.
- Established CI/CD using **CircleCI**.
+## Installation
-## Installation
1. Download this repository
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
```
mongodb+srv://madmin:@clustername.mongodb.net/?retryWrites=true&w=majority
```
+
3. Add this URL to the /backend/credentials.js file
4. Perform these commands in the main directory:
@@ -61,6 +66,10 @@ npm i
node server.js
```
-## Screenshots
+## Screenshots
-
\ No newline at end of file
+
+
+
+
+
diff --git a/backend/handlers/gameHandler.js b/backend/handlers/gameHandler.js
index b66348a..88728d3 100644
--- a/backend/handlers/gameHandler.js
+++ b/backend/handlers/gameHandler.js
@@ -1,5 +1,5 @@
const { getRoom, updateRoom } = require('../services/roomService');
-const { sendToPlayersRolledNumber } = require('../socket/emits');
+const { sendToPlayersRolledNumber, sendWinner } = require('../socket/emits');
const { rollDice, isMoveValid } = require('./handlersFunctions');
module.exports = socket => {
@@ -7,12 +7,18 @@ module.exports = socket => {
const handleMovePawn = async pawnId => {
const room = await getRoom(req.session.roomId);
+ if (room.winner) return;
const pawn = room.getPawn(pawnId);
if (isMoveValid(req.session, pawn, room)) {
const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
room.beatPawns(newPositionOfMovedPawn, req.session.color);
room.changeMovingPlayer();
+ const winner = room.getWinner();
+ if (winner) {
+ room.endGame(winner);
+ sendWinner(room._id.toString(), winner);
+ }
await updateRoom(room);
}
};
diff --git a/backend/handlers/handlersFunctions.js b/backend/handlers/handlersFunctions.js
index 74e5ed5..7c4942b 100644
--- a/backend/handlers/handlersFunctions.js
+++ b/backend/handlers/handlersFunctions.js
@@ -1,4 +1,4 @@
-const { sendToPlayersRolledNumber } = require('../socket/emits');
+const { sendToPlayersRolledNumber, sendWinner } = require('../socket/emits');
const rollDice = () => {
const rolledNumber = Math.ceil(Math.random() * 6);
@@ -8,6 +8,7 @@ const rollDice = () => {
const makeRandomMove = async roomId => {
const { updateRoom, getRoom } = require('../services/roomService');
const room = await getRoom(roomId);
+ if (room.winner) return;
if (room.rolledNumber === null) {
room.rolledNumber = rollDice();
sendToPlayersRolledNumber(room._id.toString(), room.rolledNumber);
@@ -19,6 +20,11 @@ const makeRandomMove = async roomId => {
room.movePawn(randomPawn);
}
room.changeMovingPlayer();
+ const winner = room.getWinner();
+ if (winner) {
+ room.endGame(winner);
+ sendWinner(room._id.toString(), winner);
+ }
await updateRoom(room);
};
diff --git a/backend/handlers/playerHandler.js b/backend/handlers/playerHandler.js
index 3ff80c9..93869e1 100644
--- a/backend/handlers/playerHandler.js
+++ b/backend/handlers/playerHandler.js
@@ -12,6 +12,14 @@ module.exports = socket => {
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 room = await getRoom(req.session.roomId);
room.getPlayer(req.session.playerId).changeReadyStatus();
@@ -45,4 +53,5 @@ module.exports = socket => {
socket.on('player:login', handleLogin);
socket.on('player:ready', handleReady);
+ socket.on('player:exit', handleExit);
};
diff --git a/backend/handlers/roomHandler.js b/backend/handlers/roomHandler.js
index e8a638c..396ed21 100644
--- a/backend/handlers/roomHandler.js
+++ b/backend/handlers/roomHandler.js
@@ -1,5 +1,5 @@
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
-const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits');
+const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits');
module.exports = socket => {
const req = socket.request;
@@ -13,6 +13,7 @@ module.exports = socket => {
await updateRoom(room);
}
sendToOnePlayerData(socket.id, room);
+ if (room.winner) sendWinner(socket.id, room.winner);
};
const handleGetAllRooms = async () => {
diff --git a/backend/models/room.js b/backend/models/room.js
index 8c3f25a..47fd2f7 100644
--- a/backend/models/room.js
+++ b/backend/models/room.js
@@ -15,6 +15,7 @@ const RoomSchema = new mongoose.Schema({
timeoutID: Number,
rolledNumber: Number,
players: [PlayerSchema],
+ winner: { type: String, default: null },
pawns: {
type: [PawnSchema],
default: () => {
@@ -45,6 +46,7 @@ RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
};
RoomSchema.methods.changeMovingPlayer = function () {
+ if (this.winner) return;
const playerIndex = this.players.findIndex(player => player.nowMoving === true);
this.players[playerIndex].nowMoving = false;
if (playerIndex + 1 === this.players.length) {
@@ -87,6 +89,31 @@ RoomSchema.methods.startGame = function () {
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 () {
if (this.players.length === 4) {
this.full = true;
diff --git a/backend/socket/emits.js b/backend/socket/emits.js
index e0d6eb7..36e5de4 100644
--- a/backend/socket/emits.js
+++ b/backend/socket/emits.js
@@ -16,4 +16,14 @@ const sendToOnePlayerRooms = (id, 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,
+};
diff --git a/src/components/Gameboard/Gameboard.jsx b/src/components/Gameboard/Gameboard.jsx
index 99384ac..eb1e1c0 100644
--- a/src/components/Gameboard/Gameboard.jsx
+++ b/src/components/Gameboard/Gameboard.jsx
@@ -4,11 +4,13 @@ import { PlayerDataContext, SocketContext } from '../../App';
import useSocketData from '../../hooks/useSocketData';
import Map from './Map/Map';
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 socket = useContext(SocketContext);
const context = useContext(PlayerDataContext);
-
const [pawns, setPawns] = useState([]);
const [players, setPlayers] = useState([]);
@@ -20,6 +22,8 @@ const Gameboard = () => {
const [movingPlayer, setMovingPlayer] = useState('red');
+ const [winner, setWinner] = useState(null);
+
useEffect(() => {
socket.emit('room:data', context.roomId);
socket.on('room:data', data => {
@@ -47,11 +51,18 @@ const Gameboard = () => {
setTime(data.nextMoveTime);
setStarted(data.started);
});
- }, [socket]);
+
+ socket.on('game:winner', winner => {
+ setWinner(winner);
+ });
+ socket.on('redirect', () => {
+ window.location.reload();
+ });
+ }, [socket, context.playerId, context.roomId, setRolledNumber]);
return (
<>
- {(players[0] && !started) || (time && started) ? (
+ {pawns.length === 16 ? (
{
movingPlayer={movingPlayer}
rolledNumber={rolledNumber}
nowMoving={nowMoving}
+ ended={winner !== null}
/>
) : (
)}
+ {winner ? (
+
+
+

+
+ 1st: {winner}
+
+
+
+
+ ) : null}
>
);
};
diff --git a/src/components/Gameboard/Gameboard.module.css b/src/components/Gameboard/Gameboard.module.css
new file mode 100644
index 0000000..25d9f47
--- /dev/null
+++ b/src/components/Gameboard/Gameboard.module.css
@@ -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;
+}
diff --git a/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx b/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx
index d6b9987..8a62ff7 100644
--- a/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx
+++ b/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx
@@ -14,17 +14,19 @@ const ServerListTable = ({ rooms, handleJoinClick }) => {
- {rooms.map((room, index) => (
-
- {room.private ? : null} |
- {room.name} |
- {`${room.players.length}/4`} |
- {room.isStarted ? 'started' : 'waiting'} |
-
-
- |
-
- ))}
+ {rooms.map((room, index) => {
+ return room.started ? null : (
+
+ {room.private ? : null} |
+ {room.name} |
+ {`${room.players.length}/4`} |
+ {room.isStarted ? 'started' : 'waiting'} |
+
+
+ |
+
+ );
+ })}
);
diff --git a/src/components/Navbar/Navbar.jsx b/src/components/Navbar/Navbar.jsx
index 608b6e0..38f5d43 100644
--- a/src/components/Navbar/Navbar.jsx
+++ b/src/components/Navbar/Navbar.jsx
@@ -7,7 +7,7 @@ import { useContext } from 'react';
import { PlayerDataContext } from '../../App';
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 diceProps = {
@@ -21,7 +21,7 @@ const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, movi
{players.map((player, index) => (
- {started ? : null}
+ {started && !ended ? : null}
{context.color === player.color && !started ? : null}
))}
diff --git a/src/images/lobby.png b/src/images/lobby.png
new file mode 100644
index 0000000..5bce494
Binary files /dev/null and b/src/images/lobby.png differ
diff --git a/src/images/trophy.webp b/src/images/trophy.webp
new file mode 100644
index 0000000..59dde76
Binary files /dev/null and b/src/images/trophy.webp differ
diff --git a/src/images/winner.png b/src/images/winner.png
new file mode 100644
index 0000000..7438ee7
Binary files /dev/null and b/src/images/winner.png differ