Compare commits

..

No commits in common. "develop" and "main" have entirely different histories.

31 changed files with 54 additions and 469 deletions

6
.env
View File

@ -1,8 +1,8 @@
# MongoDB connection for backend # MongoDB connection for backend
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0 CONNECTION_URI=mongodb://admin:adminpassword@mongo:27017/ludo?authSource=admin&replicaSet=rs0
# Backend port # Backend port
PORT=3000 PORT=18081
# Environment # Environment
NODE_ENV=development NODE_ENV=production

View File

@ -103,7 +103,7 @@ RUN chmod +x wait-for-mongo.sh
# Default fallback values (can be overridden by Compose) # Default fallback values (can be overridden by Compose)
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PORT=8080 ENV PORT=8080
ENV CONNECTION_URI=mongodb://192.168.0.197:27017/ludo?replicaSet=rs0 ENV CONNECTION_URI=mongodb://mongo:27017/ludo?replicaSet=rs0
EXPOSE 18081 EXPOSE 18081

View File

@ -1,5 +0,0 @@
PORT=8080
# MongoDB connection for backend
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0
NODE_ENV="development"

3
backend/.env.example Normal file
View File

@ -0,0 +1,3 @@
PORT=8080
CONNECTION_URI=your_mongodb_connection_uri
NODE_ENV="development"

View File

@ -1,11 +1,6 @@
module.exports = async function (mongoose) { module.exports = async function (mongoose) {
try { try {
console.log('🔌 Attempting to connect with URI:', process.env.CONNECTION_URI); await mongoose.connect(process.env.CONNECTION_URI);
await mongoose.connect(process.env.CONNECTION_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
directConnection: true,
});
console.log('✅ MongoDB connected'); console.log('✅ MongoDB connected');
} catch (err) { } catch (err) {
console.error('❌ MongoDB connection error:', err); console.error('❌ MongoDB connection error:', err);

View File

@ -1,41 +1,10 @@
const session = require('express-session'); const session = require('express-session');
const MongoDBStore = require('connect-mongodb-session')(session); const MongoDBStore = require('connect-mongodb-session')(session);
console.log('📋 Session.js - CONNECTION_URI:', process.env.CONNECTION_URI); const store = new MongoDBStore({
uri: process.env.CONNECTION_URI,
// Parse the connection URI to extract connection options
const uriString = process.env.CONNECTION_URI;
const uriUrl = new URL(uriString);
// Extract individual components
const baseUri = `${uriUrl.protocol}//${uriUrl.username}:${uriUrl.password}@${uriUrl.hostname}:${uriUrl.port}${uriUrl.pathname}`;
const replicaSet = uriUrl.searchParams.get('replicaSet');
const authSource = uriUrl.searchParams.get('authSource');
console.log('📋 Base URI:', baseUri);
console.log('📋 ReplicaSet:', replicaSet);
console.log('📋 AuthSource:', authSource);
// Build connection options with directConnection to bypass replica set discovery
const connectionOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
directConnection: true, // Force direct connection to specified server
};
if (authSource) {
connectionOptions.authSource = authSource;
}
const storeOptions = {
uri: baseUri,
collection: 'sessions', collection: 'sessions',
connectionOptions: connectionOptions, });
};
console.log('📋 Store options:', JSON.stringify(storeOptions, null, 2));
const store = new MongoDBStore(storeOptions);
const sessionMiddleware = session({ const sessionMiddleware = session({
store: store, store: store,
credentials: true, credentials: true,

View File

@ -12,19 +12,9 @@ module.exports = socket => {
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);
const beaten = room.beatPawns(newPositionOfMovedPawn, req.session.color); room.beatPawns(newPositionOfMovedPawn, req.session.color);
// If pawn killed any opponent pawns, attacker gets another turn.
// Also a roll of 6 grants another turn.
// Additionally, grant an extra turn when a pawn reaches its final home position.
const FINAL_POSITIONS = { red: 73, blue: 79, green: 85, yellow: 91 };
const reachedHome = FINAL_POSITIONS[pawn.color] === newPositionOfMovedPawn;
const winner = room.getWinner();
if ((beaten > 0 || room.rolledNumber === 6 || reachedHome) && !winner) {
room.resetTurnForSamePlayer();
} else {
room.changeMovingPlayer(); room.changeMovingPlayer();
} const winner = room.getWinner();
// const winner = room.getWinner();
if (winner) { if (winner) {
room.endGame(winner); room.endGame(winner);
sendWinner(room._id.toString(), winner); sendWinner(room._id.toString(), winner);

View File

@ -19,12 +19,7 @@ const makeRandomMove = async roomId => {
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)]; const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
room.movePawn(randomPawn); room.movePawn(randomPawn);
} }
// If player rolled a 6 they should get another turn: keep same player but reset timer
if (room.rolledNumber !== 6) {
room.changeMovingPlayer(); room.changeMovingPlayer();
} else {
room.resetTurnForSamePlayer();
}
const winner = room.getWinner(); const winner = room.getWinner();
if (winner) { if (winner) {
room.endGame(winner); room.endGame(winner);

View File

@ -1,4 +1,4 @@
const { getRooms, getRoom, updateRoom, createNewRoom, deleteRoom } = require('../services/roomService'); const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits'); const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits');
module.exports = socket => { module.exports = socket => {
@ -8,7 +8,6 @@ module.exports = socket => {
const room = await getRoom(req.session.roomId); const room = await getRoom(req.session.roomId);
// Handle the situation when the server crashes and any player reconnects after the time has expired // Handle the situation when the server crashes and any player reconnects after the time has expired
// Typically, the responsibility for changing players is managed by gameHandler.js. // Typically, the responsibility for changing players is managed by gameHandler.js.
if(!room) return;
if (room.nextMoveTime <= Date.now()) { if (room.nextMoveTime <= Date.now()) {
room.changeMovingPlayer(); room.changeMovingPlayer();
await updateRoom(room); await updateRoom(room);
@ -27,23 +26,7 @@ module.exports = socket => {
sendToOnePlayerRooms(socket.id, await getRooms()); sendToOnePlayerRooms(socket.id, await getRooms());
}; };
const handleDeleteRoom = async roomId => {
try {
console.log('🗑️ Attempting to delete room:', roomId);
const result = await deleteRoom(roomId);
console.log('✅ Room deleted successfully:', result);
const updatedRooms = await getRooms();
console.log('📋 Updated room count:', updatedRooms.length);
console.log('📤 Sending updated rooms to socket:', socket.id);
sendToOnePlayerRooms(socket.id, updatedRooms);
} catch (error) {
console.error('❌ Error deleting room:', error);
socket.emit('error:deleteRoom', 'Failed to delete room');
}
};
socket.on('room:data', handleGetData); socket.on('room:data', handleGetData);
socket.on('room:rooms', handleGetAllRooms); socket.on('room:rooms', handleGetAllRooms);
socket.on('room:create', handleCreateRoom); socket.on('room:create', handleCreateRoom);
socket.on('room:delete', handleDeleteRoom);
}; };

View File

@ -9,8 +9,7 @@ const PawnSchema = new Schema({
}); });
PawnSchema.methods.canMove = function (rolledNumber) { PawnSchema.methods.canMove = function (rolledNumber) {
// Pawn can leave base only when a 6 is rolled if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) {
if (this.position === this.basePos && rolledNumber === 6) {
return true; return true;
} }
// (if player's pawn is near finish line) if the move does not go beyond the win line // (if player's pawn is near finish line) if the move does not go beyond the win line

View File

@ -5,17 +5,6 @@ const timeoutManager = require('./timeoutManager.js');
const PawnSchema = require('./pawn'); const PawnSchema = require('./pawn');
const PlayerSchema = require('./player'); const PlayerSchema = require('./player');
// Safe/colored box positions in Ludo (where pawns cannot be killed)
const SAFE_POSITIONS = [16, 29, 42, 55];
// const HOME_ENTRY_POSITIONS = [66, 27, 40, 53];
const STAR_POSITIONS = [63, 24, 37, 50];
const isSafePosition = (position) => {
return SAFE_POSITIONS.includes(position)
|| STAR_POSITIONS.includes(position)
|| position > 66; // Also safe in home stretch
};
const RoomSchema = new mongoose.Schema({ const RoomSchema = new mongoose.Schema({
name: String, name: String,
private: { type: Boolean, default: false }, private: { type: Boolean, default: false },
@ -47,65 +36,30 @@ const RoomSchema = new mongoose.Schema({
}); });
RoomSchema.methods.beatPawns = function (position, attackingPawnColor) { RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
// Do not beat pawns on safe/colored positions
if (isSafePosition(position)) {
return 0;
}
let beatenCount = 0;
const pawnsOnPosition = this.pawns.filter(pawn => pawn.position === position); const pawnsOnPosition = this.pawns.filter(pawn => pawn.position === position);
pawnsOnPosition.forEach(pawn => { pawnsOnPosition.forEach(pawn => {
if (pawn.color !== attackingPawnColor) { if (pawn.color !== attackingPawnColor) {
const index = this.getPawnIndex(pawn._id); const index = this.getPawnIndex(pawn._id);
if (index !== -1 && this.pawns[index].position !== this.pawns[index].basePos) {
this.pawns[index].position = this.pawns[index].basePos; this.pawns[index].position = this.pawns[index].basePos;
beatenCount++;
}
} }
}); });
return beatenCount;
}; };
RoomSchema.methods.changeMovingPlayer = function () { RoomSchema.methods.changeMovingPlayer = function () {
if (this.winner) return; if (this.winner) return;
if (!Array.isArray(this.players) || this.players.length === 0) { const playerIndex = this.players.findIndex(player => player.nowMoving === true);
console.warn(`[room:${this._id}] changeMovingPlayer: players array is empty or null`);
return;
}
const playerIndex = this.players.findIndex(player => player && player.nowMoving === true);
if (playerIndex === -1) {
console.warn(`[room:${this._id}] changeMovingPlayer: no player currently marked as nowMoving`);
// Default to first player
this.players[0].nowMoving = true;
} else {
this.players[playerIndex].nowMoving = false; this.players[playerIndex].nowMoving = false;
if (playerIndex + 1 === this.players.length) { if (playerIndex + 1 === this.players.length) {
this.players[0].nowMoving = true; this.players[0].nowMoving = true;
} else { } else {
this.players[playerIndex + 1].nowMoving = true; this.players[playerIndex + 1].nowMoving = true;
} }
}
this.nextMoveTime = Date.now() + MOVE_TIME; this.nextMoveTime = Date.now() + MOVE_TIME;
this.rolledNumber = null; this.rolledNumber = null;
timeoutManager.clear(this._id.toString()); timeoutManager.clear(this._id.toString());
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString()); timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
}; };
RoomSchema.methods.resetTurnForSamePlayer = function () {
if (this.winner) return;
// Keep the same player moving but reset the roll and move timer
const now = Date.now();
const currentPlayer = this.getCurrentlyMovingPlayer();
if (!currentPlayer) {
console.warn(`[room:${this._id}] resetTurnForSamePlayer: No current player found!`);
return;
}
this.nextMoveTime = now + MOVE_TIME;
this.rolledNumber = null;
console.log(`[room:${this._id}] resetTurnForSamePlayer: player=${currentPlayer.color}, now=${now}, nextMoveTime=${this.nextMoveTime}, MOVE_TIME=${MOVE_TIME}`);
timeoutManager.clear(this._id.toString());
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
};
RoomSchema.methods.movePawn = function (pawn) { RoomSchema.methods.movePawn = function (pawn) {
const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber); const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber);
this.changePositionOfPawn(pawn, newPositionOfMovedPawn); this.changePositionOfPawn(pawn, newPositionOfMovedPawn);
@ -114,9 +68,8 @@ RoomSchema.methods.movePawn = function (pawn) {
RoomSchema.methods.getPawnsThatCanMove = function () { RoomSchema.methods.getPawnsThatCanMove = function () {
const movingPlayer = this.getCurrentlyMovingPlayer(); const movingPlayer = this.getCurrentlyMovingPlayer();
if (!movingPlayer) return [];
const playerPawns = this.getPlayerPawns(movingPlayer.color); const playerPawns = this.getPlayerPawns(movingPlayer.color);
return (playerPawns || []).filter(pawn => pawn.canMove(this.rolledNumber)); return playerPawns.filter(pawn => pawn.canMove(this.rolledNumber));
}; };
RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) { RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
@ -125,15 +78,10 @@ RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
}; };
RoomSchema.methods.canStartGame = function () { RoomSchema.methods.canStartGame = function () {
if (!Array.isArray(this.players)) return false; return this.players.filter(player => player.ready).length >= 2;
return this.players.filter(player => player && player.ready).length >= 2;
}; };
RoomSchema.methods.startGame = function () { RoomSchema.methods.startGame = function () {
if (!Array.isArray(this.players) || this.players.length === 0) {
console.warn(`[room:${this._id}] startGame: players array empty`);
return;
}
this.started = true; this.started = true;
this.nextMoveTime = Date.now() + MOVE_TIME; this.nextMoveTime = Date.now() + MOVE_TIME;
this.players.forEach(player => (player.ready = true)); this.players.forEach(player => (player.ready = true));
@ -145,30 +93,28 @@ RoomSchema.methods.endGame = function (winner) {
timeoutManager.clear(this._id.toString()); timeoutManager.clear(this._id.toString());
this.rolledNumber = null; this.rolledNumber = null;
this.nextMoveTime = null; this.nextMoveTime = null;
if (Array.isArray(this.players)) this.players.forEach(player => (player.nowMoving = false)); this.players.map(player => (player.nowMoving = false));
this.winner = winner; this.winner = winner;
this.save(); this.save();
}; };
RoomSchema.methods.getWinner = function () { RoomSchema.methods.getWinner = function () {
if (!Array.isArray(this.pawns)) return null; if (this.pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) {
if (this.pawns.filter(pawn => pawn && pawn.color === 'red' && pawn.position === 73).length === 4) {
return 'red'; return 'red';
} }
if (this.pawns.filter(pawn => pawn && pawn.color === 'blue' && pawn.position === 79).length === 4) { if (this.pawns.filter(pawn => pawn.color === 'blue' && pawn.position === 79).length === 4) {
return 'blue'; return 'blue';
} }
if (this.pawns.filter(pawn => pawn && pawn.color === 'green' && pawn.position === 85).length === 4) { if (this.pawns.filter(pawn => pawn.color === 'green' && pawn.position === 85).length === 4) {
return 'green'; return 'green';
} }
if (this.pawns.filter(pawn => pawn && pawn.color === 'yellow' && pawn.position === 91).length === 4) { if (this.pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) {
return 'yellow'; return 'yellow';
} }
return null; return null;
}; };
RoomSchema.methods.isFull = function () { RoomSchema.methods.isFull = function () {
if (!Array.isArray(this.players)) return false;
if (this.players.length === 4) { if (this.players.length === 4) {
this.full = true; this.full = true;
} }
@ -176,13 +122,11 @@ RoomSchema.methods.isFull = function () {
}; };
RoomSchema.methods.getPlayer = function (playerId) { RoomSchema.methods.getPlayer = function (playerId) {
if (!Array.isArray(this.players)) return null; return this.players.find(player => player._id.toString() === playerId.toString());
return this.players.find(player => player && player._id && player._id.toString() === playerId.toString()) || null;
}; };
RoomSchema.methods.addPlayer = function (name, id) { RoomSchema.methods.addPlayer = function (name, id) {
if (this.full) return; if (this.full) return;
if (!Array.isArray(this.players)) this.players = [];
this.players.push({ this.players.push({
sessionID: id, sessionID: id,
name: name, name: name,
@ -192,36 +136,19 @@ RoomSchema.methods.addPlayer = function (name, id) {
}; };
RoomSchema.methods.getPawnIndex = function (pawnId) { RoomSchema.methods.getPawnIndex = function (pawnId) {
if (!Array.isArray(this.pawns)) return -1; return this.pawns.findIndex(pawn => pawn._id.toString() === pawnId.toString());
return this.pawns.findIndex(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString());
}; };
RoomSchema.methods.getPawn = function (pawnId) { RoomSchema.methods.getPawn = function (pawnId) {
if (!Array.isArray(this.pawns)) return null; return this.pawns.find(pawn => pawn._id.toString() === pawnId.toString());
return this.pawns.find(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString()) || null;
}; };
RoomSchema.methods.getPlayerPawns = function (color) { RoomSchema.methods.getPlayerPawns = function (color) {
if (!Array.isArray(this.pawns)) return []; return this.pawns.filter(pawn => pawn.color === color);
return this.pawns.filter(pawn => pawn && pawn.color === color);
};
RoomSchema.methods.getPawnsOnPosition = function (position) {
if (!Array.isArray(this.pawns)) return [];
return this.pawns.filter(pawn => pawn && pawn.position === position);
};
RoomSchema.methods.isStacked = function (position) {
return this.getPawnsOnPosition(position).length > 1;
};
RoomSchema.methods.getOpponentPawnsOnPosition = function (position, color) {
return this.getPawnsOnPosition(position).filter(pawn => pawn.color !== color);
}; };
RoomSchema.methods.getCurrentlyMovingPlayer = function () { RoomSchema.methods.getCurrentlyMovingPlayer = function () {
if (!Array.isArray(this.players)) return null; return this.players.find(player => player.nowMoving === true);
return this.players.find(player => player && player.nowMoving === true) || null;
}; };
const Room = mongoose.model('Room', RoomSchema); const Room = mongoose.model('Room', RoomSchema);

View File

@ -3,14 +3,11 @@ const cors = require('cors');
const path = require('path'); const path = require('path');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
require('dotenv').config({ path: path.join(__dirname, '.env') }); require('dotenv').config();
const { sessionMiddleware } = require('./config/session'); const { sessionMiddleware } = require('./config/session');
const PORT = process.env.PORT || 5000; const PORT = process.env.PORT || 5000;
console.log('🔍 Environment loaded from:', path.join(__dirname, '.env'));
console.log('📍 Connection URI:', process.env.CONNECTION_URI);
const app = express(); const app = express();
app.use(cookieParser()); app.use(cookieParser());

View File

@ -23,21 +23,8 @@ const createNewRoom = async data => {
return room; return room;
}; };
const deleteRoom = async roomId => {
return await Room.findByIdAndDelete(roomId).exec();
};
Room.watch().on('change', async data => { Room.watch().on('change', async data => {
// Ignore delete operations sendToPlayersData(await getRoom(data.documentKey._id));
if (data.operationType === 'delete') {
console.log('🗑️ Room deleted, skipping sendToPlayersData');
return;
}
const room = await getRoom(data.documentKey._id);
if (room) {
sendToPlayersData(room);
}
}); });
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom, deleteRoom }; module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom };

View File

@ -1,22 +0,0 @@
const mongodb = require('mongodb');
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
// Try without replicaSet
const uri = 'mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin';
console.log('Testing without replicaSet:', uri);
mongodb.MongoClient.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
authSource: 'admin',
serverSelectionTimeoutMS: 5000,
}, (err, client) => {
if (err) {
console.error('❌ Connection error:', err.message);
process.exit(1);
} else {
console.log('✅ Connected successfully!');
client.close();
process.exit(0);
}
});

View File

@ -1,27 +0,0 @@
const mongodb = require('mongodb');
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
const uri = process.env.CONNECTION_URI;
console.log('Testing with URI:', uri);
const uriUrl = new URL(uri);
console.log('URL components:');
console.log(' hostname:', uriUrl.hostname);
console.log(' port:', uriUrl.port);
console.log(' pathname:', uriUrl.pathname);
console.log(' searchParams:', Object.fromEntries(uriUrl.searchParams));
// Try to connect directly
mongodb.MongoClient.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
replicaSet: 'rs0',
authSource: 'admin'
}, (err, client) => {
if (err) {
console.error('Connection error:', err.message);
} else {
console.log('✅ Connected successfully!');
client.close();
}
});

View File

@ -1,23 +0,0 @@
process.env.DEBUG = 'mongodb:*';
const mongodb = require('mongodb');
const uri = 'mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin';
console.log('Connecting to:', uri);
const client = new mongodb.MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
authSource: 'admin',
serverSelectionTimeoutMS: 3000,
loggerLevel: 'debug',
});
client.connect((err) => {
if (err) {
console.error('❌ Connection error:', err);
} else {
console.log('✅ Connected!');
client.close();
}
});

View File

@ -1,34 +0,0 @@
const mongodb = require('mongodb');
const net = require('net');
const dns = require('dns').promises;
async function test() {
// Test DNS resolution
console.log('Testing DNS resolution...');
try {
const res = await dns.resolve4('192.168.0.197');
console.log('DNS resolve4(192.168.0.197):', res);
} catch (e) {
console.log('DNS error:', e.message);
}
try {
const res = await dns.resolve4('mongo');
console.log('DNS resolve4(mongo):', res);
} catch (e) {
console.log('DNS mongo error:', e.message);
}
// Test direct socket connection
console.log('\nTesting direct TCP connection...');
const socket = net.createConnection(27017, '192.168.0.197');
socket.on('connect', () => {
console.log('✅ TCP connection successful to 192.168.0.197:27017');
socket.destroy();
});
socket.on('error', (err) => {
console.log('❌ TCP connection error:', err.message);
});
}
test();

1
package-lock.json generated
View File

@ -26,7 +26,6 @@
"web-vitals": "^3.5.0" "web-vitals": "^3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-private-property-in-object": "^7.16.7",
"@testing-library/jest-dom": "^6.1.5", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2", "@testing-library/react": "^14.1.2",
"cypress": "^13.6.1" "cypress": "^13.6.1"

View File

@ -13,13 +13,7 @@ function App() {
const [playerSocket, setPlayerSocket] = useState(); const [playerSocket, setPlayerSocket] = useState();
const [redirect, setRedirect] = useState(); const [redirect, setRedirect] = useState();
useEffect(() => { useEffect(() => {
let socket; const socket = io(`${window.location.protocol}//${window.location.host}`, { withCredentials: true });
if(process.env.NODE_ENV !== 'production') {
socket = io(`http://${window.location.hostname}:8080`, { withCredentials: true });
} else {
socket = io(`${window.location.protocol}//${window.location.host}`, { withCredentials: true });
}
socket.on('player:data', data => { socket.on('player:data', data => {
data = JSON.parse(data); data = JSON.parse(data);
setPlayerData(data); setPlayerData(data);
@ -78,5 +72,3 @@ function App() {
</SocketContext.Provider> </SocketContext.Provider>
); );
} }
export default App;

View File

@ -85,11 +85,7 @@ const Gameboard = () => {
<div className={styles.winnerContainer}> <div className={styles.winnerContainer}>
<img src={trophyImage} alt='winner' /> <img src={trophyImage} alt='winner' />
<h1> <h1>
{context.color === winner ? ( 1st: <span style={{ color: winner }}>{winner}</span>
<>You Won!</>
) : (
<>1st: <span style={{ color: winner }}>{winner}</span></>
)}
</h1> </h1>
<button onClick={() => socket.emit('player:exit')}>Play again</button> <button onClick={() => socket.emit('player:exit')}>Play again</button>
</div> </div>

View File

@ -6,7 +6,6 @@ import positionMapCoords from '../positions';
import pawnImages from '../../../constants/pawnImages'; import pawnImages from '../../../constants/pawnImages';
import canPawnMove from './canPawnMove'; import canPawnMove from './canPawnMove';
import getPositionAfterMove from './getPositionAfterMove'; import getPositionAfterMove from './getPositionAfterMove';
import { pawnsAt } from './pawnHelpers';
const Map = ({ pawns, nowMoving, rolledNumber }) => { const Map = ({ pawns, nowMoving, rolledNumber }) => {
const player = useContext(PlayerDataContext); const player = useContext(PlayerDataContext);
@ -15,26 +14,15 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
const [hintPawn, setHintPawn] = useState(); const [hintPawn, setHintPawn] = useState();
const paintPawn = (context, pawn, x, y, pawnWidth = 28, onDraw) => { const paintPawn = (context, pawn) => {
const pawnHeight = Math.round((pawnWidth * 30) / 35); const { x, y } = positionMapCoords[pawn.position];
const touchableArea = new Path2D(); const touchableArea = new Path2D();
const touchRadius = Math.max(6, Math.round(pawnWidth / 2)); touchableArea.arc(x, y, 12, 0, 2 * Math.PI);
touchableArea.arc(x, y, touchRadius, 0, 2 * Math.PI);
const image = new Image(); const image = new Image();
const imgSrc = pawnImages[pawn.color] || pawnImages['red']; image.src = pawnImages[pawn.color];
image.src = imgSrc; image.onload = function () {
context.drawImage(image, x - 17, y - 15, 35, 30);
const drawAndCallback = () => {
context.drawImage(image, x - Math.round(pawnWidth / 2), y - Math.round(pawnHeight / 2), pawnWidth, pawnHeight);
if (typeof onDraw === 'function') onDraw();
}; };
if (image.complete) {
drawAndCallback();
} else {
image.onload = drawAndCallback;
}
return touchableArea; return touchableArea;
}; };
@ -45,7 +33,7 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
cursorX = event.clientX - rect.left, cursorX = event.clientX - rect.left,
cursorY = event.clientY - rect.top; cursorY = event.clientY - rect.top;
for (const pawn of pawns) { for (const pawn of pawns) {
if (pawn.touchableArea && ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) { if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
if (canPawnMove(pawn, rolledNumber)) socket.emit('game:move', pawn._id); if (canPawnMove(pawn, rolledNumber)) socket.emit('game:move', pawn._id);
} }
} }
@ -62,7 +50,6 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
canvas.style.cursor = 'default'; canvas.style.cursor = 'default';
for (const pawn of pawns) { for (const pawn of pawns) {
if ( if (
pawn.touchableArea &&
ctx.isPointInPath(pawn.touchableArea, x, y) && ctx.isPointInPath(pawn.touchableArea, x, y) &&
player.color === pawn.color && player.color === pawn.color &&
canPawnMove(pawn, rolledNumber) canPawnMove(pawn, rolledNumber)
@ -87,88 +74,11 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
image.src = mapImage; image.src = mapImage;
image.onload = function () { image.onload = function () {
ctx.drawImage(image, 0, 0); ctx.drawImage(image, 0, 0);
// draw pawns grouped by position with 2-row layout
const drawn = new Set();
pawns.forEach((pawn, index) => { pawns.forEach((pawn, index) => {
if (drawn.has(pawn._id)) return; pawns[index].touchableArea = paintPawn(ctx, pawn);
const group = pawnsAt(pawns, pawn.position);
const total = group.length;
const center = positionMapCoords[pawn.position];
if (!center) return;
const slotOffsetX = 14; // horizontal spacing
const slotOffsetY = 2; // minimal vertical spacing between rows
// helper to draw a single representative pawn for a color group
// and show a small badge with the count when there are multiple pawns
const drawColorGroup = (pawnGroup, x, y, baseWidth) => {
if (!pawnGroup || pawnGroup.length === 0) return null;
const rep = pawnGroup[0];
// draw pawn and then badge after image renders so badge is on top
const touch = paintPawn(ctx, rep, x, y, baseWidth, () => {
if (pawnGroup.length > 1) {
const badgeRadius = 8; // smaller badge
const badgeX = x + Math.round(baseWidth / 2) - 4; // slightly closer to pawn edge
const badgeY = y - Math.round(baseWidth / 2) + 4;
ctx.beginPath();
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.arc(badgeX, badgeY, badgeRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.font = '10px Arial'; // smaller text
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(pawnGroup.length), badgeX, badgeY);
}
});
const foundIndex = pawns.findIndex(pp => pp._id === rep._id);
if (foundIndex !== -1) pawns[foundIndex].touchableArea = touch;
return touch;
};
// group pawns by color so same-color pawns stack vertically (tight)
const colorGroups = {};
group.forEach(g => {
if (!g) return;
if (!colorGroups[g.color]) colorGroups[g.color] = [];
colorGroups[g.color].push(g);
});
const colors = Object.keys(colorGroups);
const nColors = colors.length;
const colorSpacing = slotOffsetX; // horizontal spacing between different colors
colors.forEach((color, ci) => {
const pawnsOfColor = colorGroups[color];
const x = center.x + (ci - (nColors - 1) / 2) * colorSpacing;
const baseWidth = pawnsOfColor.length > 1 ? 20 : total === 1 ? 28 : 24;
drawColorGroup(pawnsOfColor, x, center.y, baseWidth);
pawnsOfColor.forEach(p => drawn.add(p._id));
});
// badge for >6 pawns
if (total > 6) {
const badgeX = center.x + slotOffsetX + 12;
const badgeY = center.y - slotOffsetY - 12;
ctx.beginPath();
ctx.fillStyle = 'rgba(0,0,0,0.75)';
ctx.arc(badgeX, badgeY, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.font = '11px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(total), badgeX, badgeY);
}
}); });
if (hintPawn) { if (hintPawn) {
// draw hint as a grey circle at target position paintPawn(ctx, hintPawn);
const pos = positionMapCoords[hintPawn.position];
if (pos) {
ctx.beginPath();
ctx.fillStyle = 'rgba(128,128,128,0.6)';
ctx.arc(pos.x, pos.y, 14, 0, Math.PI * 2);
ctx.fill();
}
} }
}; };
}; };

View File

@ -1,6 +1,6 @@
const canPawnMove = (pawn, rolledNumber) => { const canPawnMove = (pawn, rolledNumber) => {
// If is in base // If is in base
if ((rolledNumber === 6) && pawn.position === pawn.basePos) { if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) {
return true; return true;
// Other situations: pawn is on map or pawn is in end positions // Other situations: pawn is on map or pawn is in end positions
} else if (pawn.position !== pawn.basePos) { } else if (pawn.position !== pawn.basePos) {

View File

@ -42,7 +42,7 @@ const getPositionAfterMove = (pawn, rolledNumber) => {
return position; return position;
} }
case 'yellow': case 'yellow':
if (pawn.position + rolledNumber <= 91) { if (pawn.position + rolledNumber <= 85) {
if (position >= 12 && position <= 15) { if (position >= 12 && position <= 15) {
return 29; return 29;
} else if (position <= 67 && position + rolledNumber > 67) { } else if (position <= 67 && position + rolledNumber > 67) {

View File

@ -1,10 +0,0 @@
export const pawnsAt = (pawns, position) => {
return Array.isArray(pawns) ? pawns.filter(p => p && p.position === position) : [];
};
export const countPawnsAt = (pawns, position) => pawnsAt(pawns, position).length;
export const opponentsAt = (pawns, position, myColor) =>
pawnsAt(pawns, position).filter(p => p.color !== myColor);
export default { pawnsAt, countPawnsAt, opponentsAt };

View File

@ -20,23 +20,12 @@ const JoinServer = () => {
useEffect(() => { useEffect(() => {
socket.emit('room:rooms'); socket.emit('room:rooms');
socket.on('room:rooms', () => { socket.on('room:rooms', () => {
console.log('✅ Room list updated');
setIsLoading(false); setIsLoading(false);
}); });
socket.on('error:deleteRoom', (error) => {
console.error('❌ Delete error:', error);
alert('Failed to delete server: ' + error);
getRooms();
});
return () => {
socket.off('error:deleteRoom');
socket.off('room:rooms');
};
}, [socket]); }, [socket]);
const getRooms = () => { const getRooms = () => {
setRooms([]); setRooms([]);
setIsLoading(true);
socket.emit('room:rooms'); socket.emit('room:rooms');
}; };
@ -45,15 +34,6 @@ const JoinServer = () => {
setJoining(true); setJoining(true);
}; };
const handleDeleteClick = roomId => {
if (window.confirm('Are you sure you want to delete this server?')) {
console.log('🗑️ Deleting room:', roomId);
setIsLoading(true);
setRooms([]); // Clear the list immediately
socket.emit('room:delete', roomId);
}
};
const ServersTableWithLoading = withLoading(ServersTable); const ServersTableWithLoading = withLoading(ServersTable);
return ( return (
@ -71,7 +51,6 @@ const JoinServer = () => {
isLoading={isLoading} isLoading={isLoading}
rooms={rooms} rooms={rooms}
handleJoinClick={handleJoinClick} handleJoinClick={handleJoinClick}
handleDeleteClick={handleDeleteClick}
/> />
</div> </div>
} }

View File

@ -1,8 +1,7 @@
import lock from '../../../../images/login-page/lock.png'; import lock from '../../../../images/login-page/lock.png';
import styles from './ServersTable.module.css'; import styles from './ServersTable.module.css';
const ServerListTable = ({ rooms, handleJoinClick, handleDeleteClick }) => { const ServerListTable = ({ rooms, handleJoinClick }) => {
const safeRooms = Array.isArray(rooms) ? rooms : [];
return ( return (
<table className={styles.rooms}> <table className={styles.rooms}>
<thead> <thead>
@ -15,19 +14,15 @@ const ServerListTable = ({ rooms, handleJoinClick, handleDeleteClick }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{safeRooms.map((room, index) => { {rooms.map((room, index) => {
if (!room) return null;
return room.started ? null : ( return room.started ? null : (
<tr key={index}> <tr key={index}>
<td>{room.private ? <img src={lock} alt='private' /> : null}</td> <td>{room.private ? <img src={lock} alt='private' /> : null}</td>
<td className={styles.roomName}>{room.name}</td> <td className={styles.roomName}>{room.name}</td>
<td>{`${(room.players && room.players.length) || 0}/4`}</td> <td>{`${room.players.length}/4`}</td>
<td>{room.isStarted ? 'started' : 'waiting'}</td> <td>{room.isStarted ? 'started' : 'waiting'}</td>
<td className={styles.lastColumn}> <td className={styles.lastColumn}>
<button onClick={() => handleJoinClick(room)}>Join</button> <button onClick={() => handleJoinClick(room)}>Join</button>
<button onClick={() => handleDeleteClick(room._id)} className={styles.deleteBtn}>
Delete
</button>
</td> </td>
</tr> </tr>
); );

View File

@ -36,18 +36,8 @@
} }
.lastColumn { .lastColumn {
width: 130px; width: 70px;
} }
.firstColumn { .firstColumn {
width: 40px; width: 40px;
} }
.deleteBtn {
margin-left: 5px;
background-color: #ff4444;
color: white;
border: none;
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App.js'; import App from './App';
const container = document.getElementById('root'); const container = document.getElementById('root');
const root = ReactDOM.createRoot(container); const root = ReactDOM.createRoot(container);

View File