Update game mechanics and improve pawn movement logic; refactor connection URI and enhance server list handling
This commit is contained in:
parent
ed0b6cf589
commit
f7f8798f43
@ -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"
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
10
src/components/Gameboard/Map/pawnHelpers.js
Normal file
10
src/components/Gameboard/Map/pawnHelpers.js
Normal file
@ -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 };
|
||||
@ -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 (
|
||||
<table className={styles.rooms}>
|
||||
<thead>
|
||||
@ -14,12 +15,13 @@ const ServerListTable = ({ rooms, handleJoinClick, handleDeleteClick }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rooms.map((room, index) => {
|
||||
{safeRooms.map((room, index) => {
|
||||
if (!room) return null;
|
||||
return room.started ? null : (
|
||||
<tr key={index}>
|
||||
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||
<td className={styles.roomName}>{room.name}</td>
|
||||
<td>{`${room.players.length}/4`}</td>
|
||||
<td>{`${(room.players && room.players.length) || 0}/4`}</td>
|
||||
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
||||
<td className={styles.lastColumn}>
|
||||
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user