diff --git a/backend/.env b/backend/.env index 5ea659f..4f4688e 100644 --- a/backend/.env +++ b/backend/.env @@ -1,5 +1,5 @@ PORT=8080 # MongoDB connection for backend -CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin&replicaSet=rs0 +CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0 NODE_ENV="development" \ No newline at end of file diff --git a/backend/handlers/gameHandler.js b/backend/handlers/gameHandler.js index 88728d3..2629c54 100644 --- a/backend/handlers/gameHandler.js +++ b/backend/handlers/gameHandler.js @@ -13,7 +13,12 @@ module.exports = socket => { const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber); room.changePositionOfPawn(pawn, newPositionOfMovedPawn); room.beatPawns(newPositionOfMovedPawn, req.session.color); - room.changeMovingPlayer(); + // If player rolled a 6 they get another turn: keep same player but reset turn timer + if (room.rolledNumber !== 6) { + room.changeMovingPlayer(); + } else { + room.resetTurnForSamePlayer(); + } const winner = room.getWinner(); if (winner) { room.endGame(winner); diff --git a/backend/handlers/handlersFunctions.js b/backend/handlers/handlersFunctions.js index 7c4942b..df4c0f8 100644 --- a/backend/handlers/handlersFunctions.js +++ b/backend/handlers/handlersFunctions.js @@ -19,7 +19,12 @@ const makeRandomMove = async roomId => { const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)]; room.movePawn(randomPawn); } - room.changeMovingPlayer(); + // If player rolled a 6 they should get another turn: keep same player but reset timer + if (room.rolledNumber !== 6) { + room.changeMovingPlayer(); + } else { + room.resetTurnForSamePlayer(); + } const winner = room.getWinner(); if (winner) { room.endGame(winner); diff --git a/backend/models/pawn.js b/backend/models/pawn.js index 2680cad..03254fc 100644 --- a/backend/models/pawn.js +++ b/backend/models/pawn.js @@ -9,7 +9,8 @@ const PawnSchema = new Schema({ }); PawnSchema.methods.canMove = function (rolledNumber) { - if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) { + // Pawn can leave base only when a 6 is rolled + if (this.position === this.basePos && rolledNumber === 6) { return true; } // (if player's pawn is near finish line) if the move does not go beyond the win line diff --git a/backend/models/room.js b/backend/models/room.js index 7806ca4..3f50285 100644 --- a/backend/models/room.js +++ b/backend/models/room.js @@ -63,12 +63,22 @@ 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) { + if (!Array.isArray(this.players) || this.players.length === 0) { + 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 + 1].nowMoving = true; + this.players[playerIndex].nowMoving = false; + if (playerIndex + 1 === this.players.length) { + this.players[0].nowMoving = true; + } else { + this.players[playerIndex + 1].nowMoving = true; + } } this.nextMoveTime = Date.now() + MOVE_TIME; this.rolledNumber = null; @@ -76,6 +86,22 @@ RoomSchema.methods.changeMovingPlayer = function () { 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) { const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber); this.changePositionOfPawn(pawn, newPositionOfMovedPawn); @@ -84,8 +110,9 @@ RoomSchema.methods.movePawn = function (pawn) { RoomSchema.methods.getPawnsThatCanMove = function () { const movingPlayer = this.getCurrentlyMovingPlayer(); + if (!movingPlayer) return []; 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) { @@ -94,10 +121,15 @@ RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) { }; RoomSchema.methods.canStartGame = function () { - return this.players.filter(player => player.ready).length >= 2; + if (!Array.isArray(this.players)) return false; + return this.players.filter(player => player && player.ready).length >= 2; }; 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.nextMoveTime = Date.now() + MOVE_TIME; this.players.forEach(player => (player.ready = true)); @@ -109,28 +141,30 @@ RoomSchema.methods.endGame = function (winner) { timeoutManager.clear(this._id.toString()); this.rolledNumber = null; this.nextMoveTime = null; - this.players.map(player => (player.nowMoving = false)); + if (Array.isArray(this.players)) this.players.forEach(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) { + if (!Array.isArray(this.pawns)) return null; + if (this.pawns.filter(pawn => pawn && pawn.color === 'red' && pawn.position === 73).length === 4) { return 'red'; } - if (this.pawns.filter(pawn => pawn.color === 'blue' && pawn.position === 79).length === 4) { + if (this.pawns.filter(pawn => pawn && pawn.color === 'blue' && pawn.position === 79).length === 4) { return 'blue'; } - if (this.pawns.filter(pawn => pawn.color === 'green' && pawn.position === 85).length === 4) { + if (this.pawns.filter(pawn => pawn && pawn.color === 'green' && pawn.position === 85).length === 4) { return 'green'; } - if (this.pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) { + if (this.pawns.filter(pawn => pawn && pawn.color === 'yellow' && pawn.position === 91).length === 4) { return 'yellow'; } return null; }; RoomSchema.methods.isFull = function () { + if (!Array.isArray(this.players)) return false; if (this.players.length === 4) { this.full = true; } @@ -138,11 +172,13 @@ RoomSchema.methods.isFull = function () { }; RoomSchema.methods.getPlayer = function (playerId) { - return this.players.find(player => player._id.toString() === playerId.toString()); + if (!Array.isArray(this.players)) return null; + return this.players.find(player => player && player._id && player._id.toString() === playerId.toString()) || null; }; RoomSchema.methods.addPlayer = function (name, id) { if (this.full) return; + if (!Array.isArray(this.players)) this.players = []; this.players.push({ sessionID: id, name: name, @@ -152,19 +188,36 @@ RoomSchema.methods.addPlayer = function (name, id) { }; RoomSchema.methods.getPawnIndex = function (pawnId) { - return this.pawns.findIndex(pawn => pawn._id.toString() === pawnId.toString()); + if (!Array.isArray(this.pawns)) return -1; + return this.pawns.findIndex(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString()); }; RoomSchema.methods.getPawn = function (pawnId) { - return this.pawns.find(pawn => pawn._id.toString() === pawnId.toString()); + if (!Array.isArray(this.pawns)) return null; + return this.pawns.find(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString()) || null; }; RoomSchema.methods.getPlayerPawns = function (color) { - return this.pawns.filter(pawn => pawn.color === color); + if (!Array.isArray(this.pawns)) return []; + 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 () { - return this.players.find(player => player.nowMoving === true); + if (!Array.isArray(this.players)) return null; + return this.players.find(player => player && player.nowMoving === true) || null; }; const Room = mongoose.model('Room', RoomSchema); diff --git a/src/components/Gameboard/Map/Map.jsx b/src/components/Gameboard/Map/Map.jsx index 84637ea..f58ae15 100644 --- a/src/components/Gameboard/Map/Map.jsx +++ b/src/components/Gameboard/Map/Map.jsx @@ -6,6 +6,7 @@ import positionMapCoords from '../positions'; import pawnImages from '../../../constants/pawnImages'; import canPawnMove from './canPawnMove'; import getPositionAfterMove from './getPositionAfterMove'; +import { pawnsAt } from './pawnHelpers'; const Map = ({ pawns, nowMoving, rolledNumber }) => { const player = useContext(PlayerDataContext); @@ -14,15 +15,26 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => { const [hintPawn, setHintPawn] = useState(); - const paintPawn = (context, pawn) => { - const { x, y } = positionMapCoords[pawn.position]; + const paintPawn = (context, pawn, x, y, pawnWidth = 28, onDraw) => { + const pawnHeight = Math.round((pawnWidth * 30) / 35); const touchableArea = new Path2D(); - touchableArea.arc(x, y, 12, 0, 2 * Math.PI); + const touchRadius = Math.max(6, Math.round(pawnWidth / 2)); + touchableArea.arc(x, y, touchRadius, 0, 2 * Math.PI); const image = new Image(); - image.src = pawnImages[pawn.color]; - image.onload = function () { - context.drawImage(image, x - 17, y - 15, 35, 30); + const imgSrc = pawnImages[pawn.color] || pawnImages['red']; + image.src = imgSrc; + + 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; }; @@ -56,8 +68,6 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => { canPawnMove(pawn, rolledNumber) ) { const pawnPosition = getPositionAfterMove(pawn, rolledNumber); - console.log('previous position:', pawn.position); - console.log('Hovered pawn can move to position:', pawnPosition); if (pawnPosition) { canvas.style.cursor = 'pointer'; if (hintPawn && hintPawn.id === pawn._id) return; @@ -77,11 +87,88 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => { image.src = mapImage; image.onload = function () { ctx.drawImage(image, 0, 0); + // draw pawns grouped by position with 2-row layout + const drawn = new Set(); pawns.forEach((pawn, index) => { - pawns[index].touchableArea = paintPawn(ctx, pawn); + if (drawn.has(pawn._id)) return; + 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) { - paintPawn(ctx, hintPawn); + // draw hint as a grey circle at target position + 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(); + } } }; }; diff --git a/src/components/Gameboard/Map/canPawnMove.js b/src/components/Gameboard/Map/canPawnMove.js index 17f66b8..b0ed134 100644 --- a/src/components/Gameboard/Map/canPawnMove.js +++ b/src/components/Gameboard/Map/canPawnMove.js @@ -1,6 +1,6 @@ const canPawnMove = (pawn, rolledNumber) => { // If is in base - if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) { + if ((rolledNumber === 6) && pawn.position === pawn.basePos) { return true; // Other situations: pawn is on map or pawn is in end positions } else if (pawn.position !== pawn.basePos) { diff --git a/src/components/Gameboard/Map/pawnHelpers.js b/src/components/Gameboard/Map/pawnHelpers.js new file mode 100644 index 0000000..8a48635 --- /dev/null +++ b/src/components/Gameboard/Map/pawnHelpers.js @@ -0,0 +1,10 @@ +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 }; diff --git a/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx b/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx index 4532d34..8fdffc1 100644 --- a/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx +++ b/src/components/LoginPage/JoinServer/ServersTable/ServersTable.jsx @@ -2,6 +2,7 @@ import lock from '../../../../images/login-page/lock.png'; import styles from './ServersTable.module.css'; const ServerListTable = ({ rooms, handleJoinClick, handleDeleteClick }) => { + const safeRooms = Array.isArray(rooms) ? rooms : []; return (
| {room.private ? |
{room.name} | -{`${room.players.length}/4`} | +{`${(room.players && room.players.length) || 0}/4`} | {room.isStarted ? 'started' : 'waiting'} |