Merge pull request #5 from Wenszel/dev
updated packages, refactored code, added new login page and animations
This commit is contained in:
commit
823dd0e6bf
14
backend/config/database.js
Normal file
14
backend/config/database.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const CONNECTION_URI = require('../credentials.js');
|
||||||
|
|
||||||
|
module.exports = function (mongoose) {
|
||||||
|
mongoose.set('useFindAndModify', false);
|
||||||
|
mongoose
|
||||||
|
.connect(CONNECTION_URI, {
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('MongoDB Connected…');
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
};
|
||||||
26
backend/config/socket.js
Normal file
26
backend/config/socket.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const socketManager = require('../socket/socketManager');
|
||||||
|
const registerPlayerHandlers = require('../handlers/playerHandler');
|
||||||
|
const registerRoomHandlers = require('../handlers/roomHandler');
|
||||||
|
const registerGameHandlers = require('../handlers/gameHandler');
|
||||||
|
const { sessionMiddleware, wrap } = require('../config/session');
|
||||||
|
|
||||||
|
module.exports = function (server) {
|
||||||
|
socketManager.initialize(server);
|
||||||
|
socketManager.getIO().engine.on('initial_headers', (headers, req) => {
|
||||||
|
if (req.cookieHolder) {
|
||||||
|
headers['set-cookie'] = req.cookieHolder;
|
||||||
|
delete req.cookieHolder;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socketManager.getIO().use(wrap(sessionMiddleware));
|
||||||
|
socketManager.getIO().on('connection', socket => {
|
||||||
|
registerPlayerHandlers(socket);
|
||||||
|
registerRoomHandlers(socket);
|
||||||
|
registerGameHandlers(socket);
|
||||||
|
if (socket.request.session.roomId) {
|
||||||
|
const roomId = socket.request.session.roomId.toString();
|
||||||
|
socket.join(roomId);
|
||||||
|
socket.emit('player:data', JSON.stringify(socket.request.session));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
30
backend/controllers/roomController.js
Normal file
30
backend/controllers/roomController.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const Room = require('../models/room');
|
||||||
|
const { sendToPlayersData } = require('../socket/emits');
|
||||||
|
|
||||||
|
const getRoom = async roomId => {
|
||||||
|
return await Room.findOne({ _id: roomId }).exec();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRooms = async () => {
|
||||||
|
return await Room.find().exec();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRoom = async room => {
|
||||||
|
return await Room.findOneAndUpdate({ _id: room._id }, room).exec();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJoinableRoom = async () => {
|
||||||
|
return await Room.findOne({ full: false, started: false }).exec();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewRoom = data => {
|
||||||
|
const room = new Room(data);
|
||||||
|
room.save();
|
||||||
|
return room;
|
||||||
|
};
|
||||||
|
|
||||||
|
Room.watch().on('change', async data => {
|
||||||
|
sendToPlayersData(await getRoom(data.documentKey._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom };
|
||||||
@ -1,88 +1,33 @@
|
|||||||
const Room = require('../schemas/room');
|
const { getRoom, updateRoom } = require('../controllers/roomController');
|
||||||
const { getPawnPositionAfterMove } = require('../utils/functions');
|
const { sendToPlayersRolledNumber } = require('../socket/emits');
|
||||||
|
const { rollDice, isMoveValid } = require('./handlersFunctions');
|
||||||
|
|
||||||
module.exports = (io, socket) => {
|
module.exports = socket => {
|
||||||
const req = socket.request;
|
const req = socket.request;
|
||||||
|
|
||||||
const handleMovePawn = async pawnId => {
|
const handleMovePawn = async pawnId => {
|
||||||
const room = await getRoom();
|
const room = await getRoom(req.session.roomId);
|
||||||
const pawn = room.getPawn(pawnId);
|
const pawn = room.getPawn(pawnId);
|
||||||
if (isMoveValid(pawn, room)) {
|
if (isMoveValid(req.session, pawn, room)) {
|
||||||
const newPositionOfMovedPawn = getPawnPositionAfterMove(room.rolledNumber, pawn);
|
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);
|
||||||
handleChangeOfPlayer(room);
|
room.changeMovingPlayer();
|
||||||
|
await updateRoom(room);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRollDice = async () => {
|
const handleRollDice = async () => {
|
||||||
const rolledNumber = rollDice();
|
const rolledNumber = rollDice();
|
||||||
const room = await updateRoom({ rolledNumber: rolledNumber });
|
sendToPlayersRolledNumber(req.session.roomId, rolledNumber);
|
||||||
if (!canPlayerMove(room, rolledNumber)) {
|
const room = await updateRoom({ _id: req.session.roomId, rolledNumber: rolledNumber });
|
||||||
handleChangeOfPlayer(room);
|
const player = room.getPlayer(req.session.playerId);
|
||||||
|
if (!player.canMove(room, rolledNumber)) {
|
||||||
|
room.changeMovingPlayer();
|
||||||
|
await updateRoom(room);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rollDice = () => {
|
|
||||||
const rolledNumber = Math.ceil(Math.random() * 6);
|
|
||||||
sendToPlayersRolledNumber(rolledNumber);
|
|
||||||
return rolledNumber;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canPlayerMove = (room, rolledNumber) => {
|
|
||||||
const playerPawns = room.getPlayerPawns(req.session.color);
|
|
||||||
for (const pawn of playerPawns) {
|
|
||||||
if (pawn.canMove(rolledNumber)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMoveValid = (pawn, room) => {
|
|
||||||
if (req.session.color !== pawn.color) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (req.session.playerId !== room.getCurrentlyMovingPlayer()._id.toString()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeOfPlayer = async room => {
|
|
||||||
room.changeMovingPlayer();
|
|
||||||
room.timeoutID = setTimeout(makeRandomMove, 15000, room);
|
|
||||||
await updateRoom(room);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeRandomMove = async room => {
|
|
||||||
if (room.rolledNumber === null) room.rolledNumber = rollDice();
|
|
||||||
const pawnsThatCanMove = room.getPawnsThatCanMove()
|
|
||||||
if (pawnsThatCanMove.length > 0) {
|
|
||||||
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
|
|
||||||
room.movePawn(randomPawn);
|
|
||||||
}
|
|
||||||
await handleChangeOfPlayer(room);
|
|
||||||
};
|
|
||||||
|
|
||||||
Room.watch().on('change', async () => {
|
|
||||||
sendToPlayersData(await getRoom());
|
|
||||||
});
|
|
||||||
|
|
||||||
const getRoom = async () => {
|
|
||||||
return await Room.findOne({ _id: req.session.roomId }).exec();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRoom = async room => {
|
|
||||||
return await Room.findOneAndUpdate({ _id: req.session.roomId }, room).exec();
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendToPlayersRolledNumber = rolledNumber => {
|
|
||||||
io.to(req.session.roomId).emit('game:roll', rolledNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendToPlayersData = room => {
|
|
||||||
io.to(req.session.roomId).emit('room:data', JSON.stringify(room));
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on('game:roll', handleRollDice);
|
socket.on('game:roll', handleRollDice);
|
||||||
socket.on('game:move', handleMovePawn);
|
socket.on('game:move', handleMovePawn);
|
||||||
};
|
};
|
||||||
|
|||||||
35
backend/handlers/handlersFunctions.js
Normal file
35
backend/handlers/handlersFunctions.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const { sendToPlayersRolledNumber } = require('../socket/emits');
|
||||||
|
|
||||||
|
const rollDice = () => {
|
||||||
|
const rolledNumber = Math.ceil(Math.random() * 6);
|
||||||
|
return rolledNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeRandomMove = async roomId => {
|
||||||
|
const { updateRoom, getRoom } = require('../controllers/roomController');
|
||||||
|
const room = await getRoom(roomId);
|
||||||
|
if (room.rolledNumber === null) {
|
||||||
|
room.rolledNumber = rollDice();
|
||||||
|
sendToPlayersRolledNumber(room._id.toString(), room.rolledNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pawnsThatCanMove = room.getPawnsThatCanMove();
|
||||||
|
if (pawnsThatCanMove.length > 0) {
|
||||||
|
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
|
||||||
|
room.movePawn(randomPawn);
|
||||||
|
}
|
||||||
|
room.changeMovingPlayer();
|
||||||
|
await updateRoom(room);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMoveValid = (session, pawn, room) => {
|
||||||
|
if (session.color !== pawn.color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (session.playerId !== room.getCurrentlyMovingPlayer()._id.toString()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { rollDice, makeRandomMove, isMoveValid };
|
||||||
@ -1,121 +1,48 @@
|
|||||||
const RoomModel = require('../schemas/room');
|
const { getRoom, updateRoom } = require('../controllers/roomController');
|
||||||
const { colors } = require('../utils/constants');
|
const { colors } = require('../utils/constants');
|
||||||
const { getStartPositions } = require('../utils/functions');
|
|
||||||
|
|
||||||
module.exports = (io, socket) => {
|
module.exports = socket => {
|
||||||
const req = socket.request;
|
const req = socket.request;
|
||||||
|
|
||||||
// Function responsible for adding a player to an existing room or creating a new one
|
const handleLogin = async data => {
|
||||||
const login = data => {
|
const room = await getRoom(data.roomId);
|
||||||
// When new player login to game we are looking for not full and not started room to put player there
|
if (room.isFull()) return socket.emit('error:changeRoom');
|
||||||
RoomModel.findOne({ full: false, started: false }, function (err, room) {
|
if (room.started) return socket.emit('error:changeRoom');
|
||||||
if (room) {
|
if (room.private && room.password !== data.password) return socket.emit('error:wrongPassword');
|
||||||
// If there is one adds player to it
|
addPlayerToExistingRoom(room, data);
|
||||||
addPlayerToExistingRoom(room, data);
|
|
||||||
} else {
|
|
||||||
// If not creates new room and add player to it
|
|
||||||
createNewRoom(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function responsible for changing the player's readiness
|
const handleReady = async () => {
|
||||||
const ready = () => {
|
const room = await getRoom(req.session.roomId);
|
||||||
const { roomId, playerId } = req.session;
|
room.getPlayer(req.session.playerId).changeReadyStatus();
|
||||||
// Finds player room
|
if (room.canStartGame()) {
|
||||||
RoomModel.findOne({ _id: roomId }, function (err, room) {
|
room.startGame();
|
||||||
if (err) return err;
|
|
||||||
// Finds index of player in players array
|
|
||||||
const index = room.players.findIndex(player => player._id.toString() == playerId.toString());
|
|
||||||
// Changes player's readiness to the opposite
|
|
||||||
room.players[index].ready = !room.players[index].ready;
|
|
||||||
// If two players are ready starts game by setting the room properties
|
|
||||||
if (room.players.filter(player => player.ready).length >= 2) {
|
|
||||||
room.started = true;
|
|
||||||
room.nextMoveTime = Date.now() + 15000;
|
|
||||||
room.players.forEach(player => (player.ready = true));
|
|
||||||
room.players[0].nowMoving = true;
|
|
||||||
}
|
|
||||||
RoomModel.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: roomId,
|
|
||||||
},
|
|
||||||
room,
|
|
||||||
err => {
|
|
||||||
if (err) return err;
|
|
||||||
// Sends to all players in room game data
|
|
||||||
io.to(roomId.toString()).emit('room:data', JSON.stringify(room));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on('player:login', login);
|
|
||||||
socket.on('player:ready', ready);
|
|
||||||
|
|
||||||
function createNewRoom(data) {
|
|
||||||
const room = new RoomModel({
|
|
||||||
createDate: new Date(),
|
|
||||||
players: [
|
|
||||||
{
|
|
||||||
name: data.name,
|
|
||||||
color: colors[0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pawns: getStartPositions(),
|
|
||||||
});
|
|
||||||
// Saves new room to database
|
|
||||||
room.save().then(() => {
|
|
||||||
// Since it is not bound to an HTTP request, the session must be manually reloaded and saved
|
|
||||||
req.session.reload(err => {
|
|
||||||
if (err) return socket.disconnect();
|
|
||||||
// Saving session data
|
|
||||||
req.session.roomId = room._id.toString();
|
|
||||||
req.session.playerId = room.players[0]._id.toString();
|
|
||||||
req.session.color = room.players[0].color;
|
|
||||||
req.session.save();
|
|
||||||
// Sending data to the user, after which player will be redirected to the game
|
|
||||||
socket.join(room._id.toString());
|
|
||||||
socket.emit('player:data', JSON.stringify(req.session));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPlayerToExistingRoom(room, data) {
|
|
||||||
// Adding a new user to the room
|
|
||||||
room.players.push({
|
|
||||||
name: data.name,
|
|
||||||
ready: false,
|
|
||||||
color: colors[room.players.length],
|
|
||||||
});
|
|
||||||
let updatedRoom = { players: room.players };
|
|
||||||
// Checking if the room is full
|
|
||||||
if (room.players.length === 4) {
|
|
||||||
// Changes the properties of the room to the state to start the game
|
|
||||||
updatedRoom = {
|
|
||||||
...updatedRoom,
|
|
||||||
full: true,
|
|
||||||
started: true,
|
|
||||||
nextMoveTime: Date.now() + 15000,
|
|
||||||
pawns: getStartPositions(),
|
|
||||||
};
|
|
||||||
updatedRoom.players.forEach(player => (player.ready = true));
|
|
||||||
updatedRoom.players[0].nowMoving = true;
|
|
||||||
}
|
}
|
||||||
// Updates a room in the database
|
await updateRoom(room);
|
||||||
RoomModel.findOneAndUpdate({ _id: room._id }, updatedRoom).then(() => {
|
};
|
||||||
// Since it is not bound to an HTTP request, the session must be manually reloaded and saved
|
|
||||||
req.session.reload(err => {
|
const addPlayerToExistingRoom = async (room, data) => {
|
||||||
if (err) return socket.disconnect();
|
room.addPlayer(data.name);
|
||||||
// Saving session data
|
if (room.isFull()) {
|
||||||
req.session.roomId = room._id.toString();
|
room.startGame();
|
||||||
req.session.playerId = updatedRoom.players[updatedRoom.players.length - 1]._id.toString();
|
}
|
||||||
req.session.color = colors[updatedRoom.players.length - 1];
|
await updateRoom(room);
|
||||||
req.session.save();
|
reloadSession(room);
|
||||||
socket.join(room._id.toString());
|
};
|
||||||
// Sending data to the user, after which player will be redirected to the game
|
|
||||||
socket.emit('player:data', JSON.stringify(req.session));
|
// Since it is not bound to an HTTP request, the session must be manually reloaded and saved
|
||||||
});
|
const reloadSession = room => {
|
||||||
|
req.session.reload(err => {
|
||||||
|
if (err) return socket.disconnect();
|
||||||
|
req.session.roomId = room._id.toString();
|
||||||
|
req.session.playerId = room.players[room.players.length - 1]._id.toString();
|
||||||
|
req.session.color = colors[room.players.length - 1];
|
||||||
|
req.session.save();
|
||||||
|
socket.join(room._id.toString());
|
||||||
|
socket.emit('player:data', JSON.stringify(req.session));
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
socket.on('player:login', handleLogin);
|
||||||
|
socket.on('player:ready', handleReady);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,35 +1,31 @@
|
|||||||
const RoomModel = require('../schemas/room');
|
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../controllers/roomController');
|
||||||
|
const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits');
|
||||||
|
|
||||||
module.exports = (io, socket) => {
|
module.exports = socket => {
|
||||||
const req = socket.request;
|
const req = socket.request;
|
||||||
const getData = () => {
|
|
||||||
RoomModel.findOne({ _id: req.session.roomId }, function (err, room) {
|
const handleGetData = async () => {
|
||||||
if (!room) return err;
|
const room = await getRoom(req.session.roomId);
|
||||||
if (room.nextMoveTime <= Date.now()) {
|
// Handle the situation when the server crashes and any player reconnects after the time has expired
|
||||||
changeCurrentMovingPlayer();
|
// Typically, the responsibility for changing players is managed by gameHandler.js.
|
||||||
} else {
|
if (room.nextMoveTime <= Date.now()) {
|
||||||
io.to(req.session.roomId.toString()).emit('room:data', JSON.stringify(room));
|
room.changeMovingPlayer();
|
||||||
}
|
await updateRoom(room);
|
||||||
});
|
}
|
||||||
|
sendToOnePlayerData(socket.id, room);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on('room:data', getData);
|
const handleGetAllRooms = async () => {
|
||||||
|
let rooms = await getRooms();
|
||||||
|
sendToOnePlayerRooms(socket.id, rooms);
|
||||||
|
};
|
||||||
|
|
||||||
function changeCurrentMovingPlayer() {
|
const handleCreateRoom = async data => {
|
||||||
RoomModel.findOne({ _id: req.session.roomId }, function (err, room) {
|
createNewRoom(data);
|
||||||
if (!room) return err;
|
socket.to(socket.id).emit('room:created');
|
||||||
const index = room.players.findIndex(player => player.nowMoving === true);
|
};
|
||||||
const roomSize = room.players.length;
|
|
||||||
room.players[index].nowMoving = false;
|
socket.on('room:data', handleGetData);
|
||||||
if (index + 1 === roomSize) {
|
socket.on('room:rooms', handleGetAllRooms);
|
||||||
room.players[0].nowMoving = true;
|
socket.on('room:create', handleCreateRoom);
|
||||||
} else {
|
|
||||||
room.players[index + 1].nowMoving = true;
|
|
||||||
}
|
|
||||||
room.nextMoveTime = Date.now() + 15000;
|
|
||||||
RoomModel.findOneAndUpdate({ _id: req.session.roomId }, room, function (err, updatedRoom) {
|
|
||||||
io.to(req.session.roomId).emit('room:data', JSON.stringify(updatedRoom));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
82
backend/models/pawn.js
Normal file
82
backend/models/pawn.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const PawnSchema = new Schema({
|
||||||
|
color: String,
|
||||||
|
basePos: Number,
|
||||||
|
position: Number,
|
||||||
|
});
|
||||||
|
|
||||||
|
PawnSchema.methods.canMove = function (rolledNumber) {
|
||||||
|
if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// (if player's pawn is near finish line) if the move does not go beyond the win line
|
||||||
|
if (this.position !== this.getPositionAfterMove(rolledNumber) && this.position !== this.basePos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
PawnSchema.methods.getPositionAfterMove = function (rolledNumber) {
|
||||||
|
const { position, color } = this;
|
||||||
|
switch (color) {
|
||||||
|
case 'red':
|
||||||
|
if (position + rolledNumber <= 73) {
|
||||||
|
if (position >= 0 && position <= 3) {
|
||||||
|
return 16;
|
||||||
|
} else if (position <= 66 && position + rolledNumber >= 67) {
|
||||||
|
return position + rolledNumber + 1;
|
||||||
|
} else {
|
||||||
|
return position + rolledNumber;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
case 'blue':
|
||||||
|
if (position + rolledNumber <= 79) {
|
||||||
|
if (position >= 4 && position <= 7) {
|
||||||
|
return 55;
|
||||||
|
} else if (position <= 67 && position + rolledNumber > 67) {
|
||||||
|
return position + rolledNumber - 52;
|
||||||
|
} else if (position <= 53 && position + rolledNumber >= 54) {
|
||||||
|
return position + rolledNumber + 20;
|
||||||
|
} else {
|
||||||
|
return position + rolledNumber;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
case 'green':
|
||||||
|
if (position + rolledNumber <= 85) {
|
||||||
|
if (position >= 8 && position <= 11) {
|
||||||
|
return 42;
|
||||||
|
} else if (position <= 67 && position + rolledNumber > 67) {
|
||||||
|
return position + rolledNumber - 52;
|
||||||
|
} else if (position <= 40 && position + rolledNumber >= 41) {
|
||||||
|
return position + rolledNumber + 39;
|
||||||
|
} else {
|
||||||
|
return position + rolledNumber;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
case 'yellow':
|
||||||
|
if (position + rolledNumber <= 85) {
|
||||||
|
if (position >= 12 && position <= 15) {
|
||||||
|
return 29;
|
||||||
|
} else if (position <= 67 && position + rolledNumber > 67) {
|
||||||
|
return position + rolledNumber - 52;
|
||||||
|
} else if (position <= 27 && position + rolledNumber >= 28) {
|
||||||
|
return position + rolledNumber + 58;
|
||||||
|
} else {
|
||||||
|
return position + rolledNumber;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = PawnSchema;
|
||||||
25
backend/models/player.js
Normal file
25
backend/models/player.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
const PlayerSchema = new Schema({
|
||||||
|
sessionID: String,
|
||||||
|
name: String,
|
||||||
|
color: String,
|
||||||
|
ready: { type: Boolean, default: false },
|
||||||
|
nowMoving: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
PlayerSchema.methods.changeReadyStatus = function () {
|
||||||
|
this.ready = !this.ready;
|
||||||
|
};
|
||||||
|
|
||||||
|
PlayerSchema.methods.canMove = function (room, rolledNumber) {
|
||||||
|
const playerPawns = room.getPlayerPawns(this.color);
|
||||||
|
for (const pawn of playerPawns) {
|
||||||
|
if (pawn.canMove(rolledNumber)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = PlayerSchema;
|
||||||
@ -1,18 +1,37 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const { getPawnPositionAfterMove } = require('../utils/functions');
|
const { colors } = require('../utils/constants');
|
||||||
const Schema = mongoose.Schema;
|
const { makeRandomMove } = require('../handlers/handlersFunctions');
|
||||||
const PawnSchema = require('./pawn');
|
const PawnSchema = require('./pawn');
|
||||||
const PlayerSchema = require('./player');
|
const PlayerSchema = require('./player');
|
||||||
|
|
||||||
const RoomSchema = new Schema({
|
const RoomSchema = new mongoose.Schema({
|
||||||
createDate: Date,
|
name: String,
|
||||||
|
private: { type: Boolean, default: false },
|
||||||
|
password: String,
|
||||||
|
createDate: { type: Date, default: Date.now },
|
||||||
started: { type: Boolean, default: false },
|
started: { type: Boolean, default: false },
|
||||||
full: { type: Boolean, default: false },
|
full: { type: Boolean, default: false },
|
||||||
nextMoveTime: Number,
|
nextMoveTime: Number,
|
||||||
timeoutID: Number,
|
timeoutID: Number,
|
||||||
rolledNumber: Number,
|
rolledNumber: Number,
|
||||||
players: [PlayerSchema],
|
players: [PlayerSchema],
|
||||||
pawns: [PawnSchema],
|
pawns: {
|
||||||
|
type: [PawnSchema],
|
||||||
|
default: () => {
|
||||||
|
const startPositions = [];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
let pawn = {};
|
||||||
|
pawn.basePos = i;
|
||||||
|
pawn.position = i;
|
||||||
|
if (i < 4) pawn.color = colors[0];
|
||||||
|
else if (i < 8) pawn.color = colors[1];
|
||||||
|
else if (i < 12) pawn.color = colors[2];
|
||||||
|
else if (i < 16) pawn.color = colors[3];
|
||||||
|
startPositions.push(pawn);
|
||||||
|
}
|
||||||
|
return startPositions;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
|
RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
|
||||||
@ -36,10 +55,11 @@ RoomSchema.methods.changeMovingPlayer = function () {
|
|||||||
this.nextMoveTime = Date.now() + 15000;
|
this.nextMoveTime = Date.now() + 15000;
|
||||||
this.rolledNumber = null;
|
this.rolledNumber = null;
|
||||||
if (this.timeoutID) clearTimeout(this.timeoutID);
|
if (this.timeoutID) clearTimeout(this.timeoutID);
|
||||||
|
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.movePawn = function (pawn) {
|
RoomSchema.methods.movePawn = function (pawn) {
|
||||||
const newPositionOfMovedPawn = getPawnPositionAfterMove(this.rolledNumber, pawn);
|
const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber);
|
||||||
this.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
this.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
||||||
this.beatPawns(newPositionOfMovedPawn, pawn.color);
|
this.beatPawns(newPositionOfMovedPawn, pawn.color);
|
||||||
};
|
};
|
||||||
@ -48,13 +68,46 @@ RoomSchema.methods.getPawnsThatCanMove = function () {
|
|||||||
const movingPlayer = this.getCurrentlyMovingPlayer();
|
const movingPlayer = this.getCurrentlyMovingPlayer();
|
||||||
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) {
|
||||||
const pawnIndex = this.getPawnIndex(pawn._id);
|
const pawnIndex = this.getPawnIndex(pawn._id);
|
||||||
this.pawns[pawnIndex].position = newPosition;
|
this.pawns[pawnIndex].position = newPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.canStartGame = function () {
|
||||||
|
return this.players.filter(player => player.ready).length >= 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.startGame = function () {
|
||||||
|
this.started = true;
|
||||||
|
this.nextMoveTime = Date.now() + 15000;
|
||||||
|
this.players.forEach(player => (player.ready = true));
|
||||||
|
this.players[0].nowMoving = true;
|
||||||
|
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.isFull = function () {
|
||||||
|
if (this.players.length === 4) {
|
||||||
|
this.full = true;
|
||||||
|
}
|
||||||
|
return this.full;
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.getPlayer = function (playerId) {
|
||||||
|
return this.players.find(player => player._id.toString() === playerId.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomSchema.methods.addPlayer = function (name, id) {
|
||||||
|
if (this.full) return;
|
||||||
|
this.players.push({
|
||||||
|
sessionID: id,
|
||||||
|
name: name,
|
||||||
|
ready: false,
|
||||||
|
color: colors[this.players.length],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getPawnIndex = function (pawnId) {
|
RoomSchema.methods.getPawnIndex = function (pawnId) {
|
||||||
return this.pawns.findIndex(pawn => pawn._id.toString() === pawnId.toString());
|
return this.pawns.findIndex(pawn => pawn._id.toString() === pawnId.toString());
|
||||||
};
|
};
|
||||||
@ -71,6 +124,6 @@ RoomSchema.methods.getCurrentlyMovingPlayer = function () {
|
|||||||
return this.players.find(player => player.nowMoving === true);
|
return this.players.find(player => player.nowMoving === true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomModel = mongoose.model('Room', RoomSchema);
|
const Room = mongoose.model('Room', RoomSchema);
|
||||||
|
|
||||||
module.exports = RoomModel;
|
module.exports = Room;
|
||||||
1887
backend/package-lock.json
generated
1887
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,5 +9,13 @@
|
|||||||
"express-session": "^1.17.1",
|
"express-session": "^1.17.1",
|
||||||
"mongoose": "^5.12.0",
|
"mongoose": "^5.12.0",
|
||||||
"socket.io": "^4.5.1"
|
"socket.io": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"socket.io-client": "^4.7.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha tests/**/*.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const { getPawnPositionAfterMove } = require('../utils/functions');
|
|
||||||
|
|
||||||
const PawnSchema = new Schema({
|
|
||||||
color: String,
|
|
||||||
basePos: Number,
|
|
||||||
position: Number,
|
|
||||||
});
|
|
||||||
|
|
||||||
PawnSchema.methods.canMove = function (rolledNumber) {
|
|
||||||
if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// (if player's pawn is near finish line) if the move does not go beyond the win line
|
|
||||||
if (this.position !== getPawnPositionAfterMove(rolledNumber, this) && this.position !== this.basePos) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = PawnSchema;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
|
||||||
|
|
||||||
const PlayerSchema = new Schema({
|
|
||||||
name: String,
|
|
||||||
color: String,
|
|
||||||
ready: { type: Boolean, default: false },
|
|
||||||
nowMoving: { type: Boolean, default: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = PlayerSchema;
|
|
||||||
@ -1,13 +1,11 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const { sessionMiddleware, wrap } = require('./controllers/serverController');
|
|
||||||
const registerPlayerHandlers = require('./handlers/playerHandler');
|
|
||||||
const registerRoomHandlers = require('./handlers/roomHandler');
|
|
||||||
const registerGameHandlers = require('./handlers/gameHandler');
|
|
||||||
const PORT = 8080;
|
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const CONNECTION_URI = require('./credentials.js');
|
const { sessionMiddleware } = require('./config/session');
|
||||||
|
|
||||||
|
const PORT = 8080;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
@ -26,64 +24,10 @@ app.use(
|
|||||||
);
|
);
|
||||||
app.use(sessionMiddleware);
|
app.use(sessionMiddleware);
|
||||||
|
|
||||||
mongoose.set('useFindAndModify', false);
|
const server = app.listen(PORT);
|
||||||
mongoose
|
|
||||||
.connect(CONNECTION_URI, {
|
|
||||||
useNewUrlParser: true,
|
|
||||||
useUnifiedTopology: true,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
console.log('MongoDB Connected…');
|
|
||||||
})
|
|
||||||
.catch(err => console.error(err));
|
|
||||||
|
|
||||||
const server = app.listen(PORT, () => {
|
require('./config/database')(mongoose);
|
||||||
console.log('Server runs on port ' + PORT);
|
require('./config/socket')(server);
|
||||||
});
|
|
||||||
|
|
||||||
const io = require('socket.io')(server, {
|
|
||||||
cors: {
|
|
||||||
origin: 'http://localhost:3000',
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
allowRequest: (req, callback) => {
|
|
||||||
const fakeRes = {
|
|
||||||
getHeader() {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
setHeader(key, values) {
|
|
||||||
req.cookieHolder = values[0];
|
|
||||||
},
|
|
||||||
writeHead() {},
|
|
||||||
};
|
|
||||||
sessionMiddleware(req, fakeRes, () => {
|
|
||||||
if (req.session) {
|
|
||||||
fakeRes.writeHead();
|
|
||||||
req.session.save();
|
|
||||||
}
|
|
||||||
callback(null, true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
io.engine.on('initial_headers', (headers, req) => {
|
|
||||||
if (req.cookieHolder) {
|
|
||||||
headers['set-cookie'] = req.cookieHolder;
|
|
||||||
delete req.cookieHolder;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
io.use(wrap(sessionMiddleware));
|
|
||||||
|
|
||||||
io.on('connection', socket => {
|
|
||||||
registerPlayerHandlers(io, socket);
|
|
||||||
registerRoomHandlers(io, socket);
|
|
||||||
registerGameHandlers(io, socket);
|
|
||||||
if (socket.request.session.roomId) {
|
|
||||||
const roomId = socket.request.session.roomId.toString();
|
|
||||||
socket.join(roomId);
|
|
||||||
socket.emit('player:data', JSON.stringify(socket.request.session));
|
|
||||||
io.to(roomId).emit('player joined');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
app.use(express.static('/app/build'));
|
app.use(express.static('/app/build'));
|
||||||
@ -91,3 +35,5 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
res.sendFile('/app/build/index.html');
|
res.sendFile('/app/build/index.html');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = { server };
|
||||||
|
|||||||
19
backend/socket/emits.js
Normal file
19
backend/socket/emits.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const socketManager = require('./socketManager');
|
||||||
|
|
||||||
|
const sendToPlayersRolledNumber = (id, rolledNumber) => {
|
||||||
|
socketManager.getIO().to(id).emit('game:roll', rolledNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToPlayersData = room => {
|
||||||
|
socketManager.getIO().to(room._id.toString()).emit('room:data', JSON.stringify(room));
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToOnePlayerData = (id, room) => {
|
||||||
|
socketManager.getIO().to(id).emit('room:data', JSON.stringify(room));
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToOnePlayerRooms = (id, rooms) => {
|
||||||
|
socketManager.getIO().to(id).emit('room:rooms', JSON.stringify(rooms));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { sendToPlayersData, sendToPlayersRolledNumber, sendToOnePlayerData, sendToOnePlayerRooms };
|
||||||
39
backend/socket/socketManager.js
Normal file
39
backend/socket/socketManager.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const { sessionMiddleware } = require('../config/session');
|
||||||
|
|
||||||
|
const socketManager = {
|
||||||
|
io: null,
|
||||||
|
initialize(server) {
|
||||||
|
this.io = require('socket.io')(server, {
|
||||||
|
cors: {
|
||||||
|
origin: 'http://localhost:3000',
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
allowRequest: (req, callback) => {
|
||||||
|
const fakeRes = {
|
||||||
|
getHeader() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
setHeader(key, values) {
|
||||||
|
req.cookieHolder = values[0];
|
||||||
|
},
|
||||||
|
writeHead() {},
|
||||||
|
};
|
||||||
|
sessionMiddleware(req, fakeRes, () => {
|
||||||
|
if (req.session) {
|
||||||
|
fakeRes.writeHead();
|
||||||
|
req.session.save();
|
||||||
|
}
|
||||||
|
callback(null, true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getIO() {
|
||||||
|
if (!this.io) {
|
||||||
|
throw new Error('Socket.io not initialized');
|
||||||
|
}
|
||||||
|
return this.io;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = socketManager;
|
||||||
123
backend/tests/handlers/player.test.js
Normal file
123
backend/tests/handlers/player.test.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
const { io } = require('socket.io-client');
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const { server } = require('../../server');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const CONNECTION_URI = require('../../credentials.js');
|
||||||
|
|
||||||
|
const socketURL = 'http://localhost:8080';
|
||||||
|
const options = {
|
||||||
|
transports: ['websocket'],
|
||||||
|
'force new connection': true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Testing player socket handlers', function () {
|
||||||
|
let firstPlayer, secondPlayer;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await mongoose.connect(CONNECTION_URI, {
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
firstPlayer = io.connect(socketURL, options);
|
||||||
|
secondPlayer = io.connect(socketURL, options);
|
||||||
|
await assertDatabaseIsClear();
|
||||||
|
});
|
||||||
|
const assertDatabaseIsClear = async () => {
|
||||||
|
const collectionInfo = await mongoose.connection.db.listCollections({ name: 'rooms' }).next();
|
||||||
|
if (collectionInfo) await mongoose.connection.collections.rooms.drop();
|
||||||
|
};
|
||||||
|
beforeEach(function (done) {
|
||||||
|
firstPlayer.off('room:data');
|
||||||
|
secondPlayer.off('room:data');
|
||||||
|
firstPlayer.off('player:data');
|
||||||
|
secondPlayer.off('player:data');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
if (firstPlayer.connected) {
|
||||||
|
firstPlayer.disconnect();
|
||||||
|
}
|
||||||
|
if (secondPlayer.connected) {
|
||||||
|
secondPlayer.disconnect();
|
||||||
|
}
|
||||||
|
server.close();
|
||||||
|
assertDatabaseIsClear();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return credentials when joining room', function (done) {
|
||||||
|
firstPlayer.emit('player:login', { name: 'test1' });
|
||||||
|
firstPlayer.on('player:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
expect(data).to.have.property('roomId');
|
||||||
|
expect(data).to.have.property('playerId');
|
||||||
|
expect(data).to.have.property('color');
|
||||||
|
expect(data.color).to.equal('red');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly join player to room', function (done) {
|
||||||
|
firstPlayer.emit('room:data');
|
||||||
|
firstPlayer.on('room:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
expect(data.players[0].name).to.equal('test1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly join player to existing room', function (done) {
|
||||||
|
secondPlayer.emit('player:login', { name: 'test2' });
|
||||||
|
secondPlayer.on('player:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
expect(data.color).to.equal('blue');
|
||||||
|
secondPlayer.emit('room:data');
|
||||||
|
});
|
||||||
|
secondPlayer.on('room:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
expect(data.players[1].name).to.equal('test2');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly change player ready status to true', function (done) {
|
||||||
|
firstPlayer.emit('player:ready');
|
||||||
|
firstPlayer.on('room:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
const player = data.players.find(player => player.name === 'test1');
|
||||||
|
expect(player.ready).to.equal(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly change player ready status to false', function (done) {
|
||||||
|
firstPlayer.emit('player:ready');
|
||||||
|
firstPlayer.on('room:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
const player = data.players.find(player => player.name === 'test1');
|
||||||
|
expect(player.ready).to.equal(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly change second player ready status to true', function (done) {
|
||||||
|
secondPlayer.emit('player:ready');
|
||||||
|
secondPlayer.on('room:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
const player = data.players.find(player => player.name === 'test2');
|
||||||
|
expect(player.ready).to.equal(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start game', function (done) {
|
||||||
|
firstPlayer.emit('player:ready');
|
||||||
|
firstPlayer.on('room:data', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
expect(data.started).to.equal(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
71
backend/tests/schemas/room.test.js
Normal file
71
backend/tests/schemas/room.test.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
const { expect } = require('chai');
|
||||||
|
const RoomModel = require('../../schemas/room');
|
||||||
|
const { getPawnPositionAfterMove, getStartPositions } = require('../../utils/functions');
|
||||||
|
describe('Testing room model methods', function () {
|
||||||
|
const room = new RoomModel();
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
room.players = [];
|
||||||
|
room.pawns = getStartPositions();
|
||||||
|
});
|
||||||
|
it('should correctly beat pawn', function () {
|
||||||
|
room.addPlayer('test1', 'red');
|
||||||
|
room.addPlayer('test2', 'blue');
|
||||||
|
room.pawns.forEach(pawn => {
|
||||||
|
pawn.position = getPawnPositionAfterMove(1, pawn);
|
||||||
|
});
|
||||||
|
room.beatPawns(16, 'green');
|
||||||
|
room.pawns.forEach(pawn => {
|
||||||
|
if (pawn.color != 'red') {
|
||||||
|
expect(pawn.position).to.not.equal(pawn.basePos);
|
||||||
|
} else {
|
||||||
|
expect(pawn.position).to.equal(pawn.basePos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly beat multiple pawns', function () {
|
||||||
|
room.pawns[0].position = 16;
|
||||||
|
room.pawns[1].position = 16;
|
||||||
|
room.beatPawns(16, 'green');
|
||||||
|
room.pawns.forEach(pawn => {
|
||||||
|
expect(pawn.position).to.equal(pawn.basePos);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly change moving player from last to first', function () {
|
||||||
|
room.addPlayer('test1', 'red');
|
||||||
|
room.addPlayer('test2', 'blue');
|
||||||
|
room.players[1].nowMoving = true;
|
||||||
|
room.changeMovingPlayer();
|
||||||
|
expect(room.players[0].nowMoving).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly change moving player from first to second', function () {
|
||||||
|
room.addPlayer('test1', 'red');
|
||||||
|
room.addPlayer('test2', 'blue');
|
||||||
|
room.players[0].nowMoving = true;
|
||||||
|
room.changeMovingPlayer();
|
||||||
|
expect(room.players[1].nowMoving).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly returns pawns that can move', function () {
|
||||||
|
room.addPlayer('test1', 'red');
|
||||||
|
room.addPlayer('test2', 'blue');
|
||||||
|
room.players[0].nowMoving = true;
|
||||||
|
room.pawns[0].position = 16;
|
||||||
|
room.rolledNumber = 2;
|
||||||
|
const pawnsThatCanMove = room.getPawnsThatCanMove();
|
||||||
|
expect(pawnsThatCanMove.length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should given rolled 6 correctly returns pawns that can move', function () {
|
||||||
|
room.addPlayer('test1', 'red');
|
||||||
|
room.addPlayer('test2', 'blue');
|
||||||
|
room.players[0].nowMoving = true;
|
||||||
|
room.pawns[0].position = 16;
|
||||||
|
room.rolledNumber = 6;
|
||||||
|
const pawnsThatCanMove = room.getPawnsThatCanMove();
|
||||||
|
expect(pawnsThatCanMove.length).to.equal(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
12910
package-lock.json
generated
12910
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -3,21 +3,24 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.3",
|
"@emotion/react": "^11.11.1",
|
||||||
"@testing-library/jest-dom": "^5.11.9",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@testing-library/react": "^11.2.5",
|
"@mui/material": "^5.14.20",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"axios": "^0.21.1",
|
"@testing-library/react": "^14.1.2",
|
||||||
|
"@testing-library/user-event": "^14.5.1",
|
||||||
|
"axios": "^1.6.2",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.1",
|
"react": "^18.2.0",
|
||||||
"react-beforeunload": "^2.4.0",
|
"react-beforeunload": "^2.6.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^18.2.0",
|
||||||
"react-loading": "^2.0.3",
|
"react-loading": "^2.0.3",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"socket.io": "^4.5.1",
|
"react-transition-group": "^4.4.5",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io": "^4.7.2",
|
||||||
"web-vitals": "^1.1.0"
|
"socket.io-client": "^4.7.2",
|
||||||
|
"web-vitals": "^3.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
73
src/App.js
73
src/App.js
@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState, createContext } from 'react';
|
import React, { useEffect, useState, createContext } from 'react';
|
||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import ReactLoading from 'react-loading';
|
||||||
import Gameboard from './components/Gameboard';
|
import Gameboard from './components/Gameboard/Gameboard';
|
||||||
import NameInput from './components/NameInput';
|
import LoginPage from './components/LoginPage/LoginPage';
|
||||||
|
|
||||||
export const PlayerDataContext = createContext();
|
export const PlayerDataContext = createContext();
|
||||||
export const SocketContext = createContext();
|
export const SocketContext = createContext();
|
||||||
@ -12,13 +12,14 @@ function App() {
|
|||||||
const [playerData, setPlayerData] = useState();
|
const [playerData, setPlayerData] = useState();
|
||||||
const [playerSocket, setPlayerSocket] = useState();
|
const [playerSocket, setPlayerSocket] = useState();
|
||||||
const [redirect, setRedirect] = useState();
|
const [redirect, setRedirect] = useState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = io('http://localhost:8080', { withCredentials: true });
|
const socket = io('http://localhost:8080', { withCredentials: true });
|
||||||
socket.on('player:data', data => {
|
socket.on('player:data', data => {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
setPlayerData(data);
|
setPlayerData(data);
|
||||||
data.roomId != null ? setRedirect(true) : setRedirect(false);
|
if (data.roomId != null) {
|
||||||
|
setRedirect(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setPlayerSocket(socket);
|
setPlayerSocket(socket);
|
||||||
}, []);
|
}, []);
|
||||||
@ -26,25 +27,47 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={playerSocket}>
|
<SocketContext.Provider value={playerSocket}>
|
||||||
<Router>
|
<Router>
|
||||||
{redirect ? <Redirect to='/game' /> : <Redirect to='/login' />}
|
<Routes>
|
||||||
<Switch>
|
<Route
|
||||||
<Route exact path='/'>
|
exact
|
||||||
LOADING...
|
path='/'
|
||||||
</Route>
|
Component={() => {
|
||||||
<Route path='/login'>
|
if (redirect) {
|
||||||
<NameInput />
|
return <Navigate to='/game' />;
|
||||||
</Route>
|
} else if (playerSocket) {
|
||||||
<Route path='/game'>
|
return <LoginPage />;
|
||||||
{playerData ? (
|
} else {
|
||||||
<PlayerDataContext.Provider value={playerData}>
|
return <ReactLoading type='spinningBubbles' color='white' height={667} width={375} />;
|
||||||
<Gameboard />
|
}
|
||||||
</PlayerDataContext.Provider>
|
}}
|
||||||
) : null}
|
></Route>
|
||||||
<a href='https://www.flaticon.com/free-icons/hand' title='hand icons'>
|
<Route
|
||||||
Hand icons created by berkahicon - Flaticon
|
path='/login'
|
||||||
</a>
|
Component={() => {
|
||||||
</Route>
|
if (redirect) {
|
||||||
</Switch>
|
return <Navigate to='/game' />;
|
||||||
|
} else if (playerSocket) {
|
||||||
|
return <LoginPage />;
|
||||||
|
} else {
|
||||||
|
return <ReactLoading type='spinningBubbles' color='white' height={667} width={375} />;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></Route>
|
||||||
|
<Route
|
||||||
|
path='/game'
|
||||||
|
Component={() => {
|
||||||
|
if (playerData) {
|
||||||
|
return (
|
||||||
|
<PlayerDataContext.Provider value={playerData}>
|
||||||
|
<Gameboard />
|
||||||
|
</PlayerDataContext.Provider>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Navigate to='/login' />;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></Route>
|
||||||
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</SocketContext.Provider>
|
</SocketContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useEffect, useContext } from 'react';
|
||||||
import { SocketContext } from '../../App';
|
import { SocketContext } from '../../../App';
|
||||||
import one from '../../images/dice/1.png';
|
import one from '../../../images/dice/1.png';
|
||||||
import two from '../../images/dice/2.png';
|
import two from '../../../images/dice/2.png';
|
||||||
import three from '../../images/dice/3.png';
|
import three from '../../../images/dice/3.png';
|
||||||
import four from '../../images/dice/4.png';
|
import four from '../../../images/dice/4.png';
|
||||||
import five from '../../images/dice/5.png';
|
import five from '../../../images/dice/5.png';
|
||||||
import six from '../../images/dice/6.png';
|
import six from '../../../images/dice/6.png';
|
||||||
import roll from '../../images/dice/roll.png';
|
import roll from '../../../images/dice/roll.png';
|
||||||
|
|
||||||
const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => {
|
const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => {
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
const [images] = useState([one, two, three, four, five, six, roll]);
|
|
||||||
|
const images = [one, two, three, four, five, six, roll];
|
||||||
|
|
||||||
const handleRoll = () => {
|
const handleRoll = () => {
|
||||||
socket.emit('game:roll');
|
socket.emit('game:roll');
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('game:roll', number => {
|
socket.on('game:roll', number => {
|
||||||
rolledNumberCallback(number);
|
rolledNumberCallback(number);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [socket, rolledNumberCallback]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`dice-container dice-${color}`}>
|
<div className={`dice-container dice-${color}`}>
|
||||||
{movingPlayer === color ? (
|
{movingPlayer === color ? (
|
||||||
@ -1,17 +1,16 @@
|
|||||||
import React, { useState, useEffect, useContext, useCallback } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import ReactLoading from 'react-loading';
|
import ReactLoading from 'react-loading';
|
||||||
import { PlayerDataContext, SocketContext } from '../App';
|
import { PlayerDataContext, SocketContext } from '../../App';
|
||||||
import Map from './game-board-components/Map';
|
import Map from './Map/Map';
|
||||||
import Navbar from './Navbar';
|
import Navbar from '../Navbar/Navbar';
|
||||||
|
|
||||||
const Gameboard = () => {
|
const Gameboard = () => {
|
||||||
// Context data
|
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
const context = useContext(PlayerDataContext);
|
const context = useContext(PlayerDataContext);
|
||||||
// Render data
|
|
||||||
const [pawns, setPawns] = useState([]);
|
const [pawns, setPawns] = useState([]);
|
||||||
const [players, setPlayers] = useState([]);
|
const [players, setPlayers] = useState([]);
|
||||||
// Game logic data
|
|
||||||
const [rolledNumber, setRolledNumber] = useState(null);
|
const [rolledNumber, setRolledNumber] = useState(null);
|
||||||
const [time, setTime] = useState();
|
const [time, setTime] = useState();
|
||||||
const [isReady, setIsReady] = useState();
|
const [isReady, setIsReady] = useState();
|
||||||
@ -19,22 +18,12 @@ const Gameboard = () => {
|
|||||||
const [started, setStarted] = useState(false);
|
const [started, setStarted] = useState(false);
|
||||||
|
|
||||||
const [movingPlayer, setMovingPlayer] = useState('red');
|
const [movingPlayer, setMovingPlayer] = useState('red');
|
||||||
const checkWin = useCallback(() => {
|
|
||||||
// Player wins when all pawns with same color are inside end base
|
|
||||||
if (pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) {
|
|
||||||
alert('Red Won');
|
|
||||||
} else if (pawns.filter(pawn => pawn.color === 'blue' && pawn.position === 79).length === 4) {
|
|
||||||
alert('Blue Won');
|
|
||||||
} else if (pawns.filter(pawn => pawn.color === 'green' && pawn.position === 85).length === 4) {
|
|
||||||
alert('Green Won');
|
|
||||||
} else if (pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) {
|
|
||||||
alert('Yellow Won');
|
|
||||||
}
|
|
||||||
}, [pawns]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.emit('room:data', context.roomId);
|
socket.emit('room:data', context.roomId);
|
||||||
socket.on('room:data', data => {
|
socket.on('room:data', data => {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
|
if (data.players == null) return;
|
||||||
// Filling navbar with empty player nick container
|
// Filling navbar with empty player nick container
|
||||||
while (data.players.length !== 4) {
|
while (data.players.length !== 4) {
|
||||||
data.players.push({ name: '...' });
|
data.players.push({ name: '...' });
|
||||||
@ -50,7 +39,6 @@ const Gameboard = () => {
|
|||||||
setMovingPlayer(nowMovingPlayer.color);
|
setMovingPlayer(nowMovingPlayer.color);
|
||||||
}
|
}
|
||||||
const currentPlayer = data.players.find(player => player._id === context.playerId);
|
const currentPlayer = data.players.find(player => player._id === context.playerId);
|
||||||
checkWin();
|
|
||||||
setIsReady(currentPlayer.ready);
|
setIsReady(currentPlayer.ready);
|
||||||
setRolledNumber(data.rolledNumber);
|
setRolledNumber(data.rolledNumber);
|
||||||
setPlayers(data.players);
|
setPlayers(data.players);
|
||||||
@ -58,17 +46,16 @@ const Gameboard = () => {
|
|||||||
setTime(data.nextMoveTime);
|
setTime(data.nextMoveTime);
|
||||||
setStarted(data.started);
|
setStarted(data.started);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [socket]);
|
||||||
|
|
||||||
// Callback to handle dice rolling between dice and map component
|
|
||||||
const rolledNumberCallback = number => {
|
const rolledNumberCallback = number => {
|
||||||
setRolledNumber(number);
|
setRolledNumber(number);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{players ? (
|
{(players[0] && !started) || (time && started) ? (
|
||||||
<>
|
<div className='container'>
|
||||||
<Navbar
|
<Navbar
|
||||||
players={players}
|
players={players}
|
||||||
started={started}
|
started={started}
|
||||||
@ -80,7 +67,7 @@ const Gameboard = () => {
|
|||||||
rolledNumberCallback={rolledNumberCallback}
|
rolledNumberCallback={rolledNumberCallback}
|
||||||
/>
|
/>
|
||||||
<Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} />
|
<Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} />
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ReactLoading type='spinningBubbles' color='white' height={667} width={375} />
|
<ReactLoading type='spinningBubbles' color='white' height={667} width={375} />
|
||||||
)}
|
)}
|
||||||
111
src/components/Gameboard/Map/Map.jsx
Normal file
111
src/components/Gameboard/Map/Map.jsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useContext } from 'react';
|
||||||
|
import { PlayerDataContext, SocketContext } from '../../../App';
|
||||||
|
|
||||||
|
import mapImage from '../../../images/map.jpg';
|
||||||
|
import positions from '../positions';
|
||||||
|
import pawnImages from '../../../constants/pawnImages';
|
||||||
|
import canPawnMove from './canPawnMove';
|
||||||
|
import getPositionAfterMove from './getPositionAfterMove';
|
||||||
|
|
||||||
|
const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||||
|
const player = useContext(PlayerDataContext);
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
|
const [hintPawn, setHintPawn] = useState();
|
||||||
|
|
||||||
|
const paintPawn = (context, x, y, color) => {
|
||||||
|
const touchableArea = new Path2D();
|
||||||
|
touchableArea.arc(x, y, 12, 0, 2 * Math.PI);
|
||||||
|
const image = new Image();
|
||||||
|
image.src = pawnImages[color];
|
||||||
|
// image.onload = function () {
|
||||||
|
context.drawImage(image, x - 17, y - 14, 35, 30);
|
||||||
|
return touchableArea;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCanvasClick = event => {
|
||||||
|
if (hintPawn) {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const rect = canvas.getBoundingClientRect(),
|
||||||
|
cursorX = event.clientX - rect.left,
|
||||||
|
cursorY = event.clientY - rect.top;
|
||||||
|
for (const pawn of pawns) {
|
||||||
|
if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
|
||||||
|
socket.emit('game:move', pawn._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHintPawn(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = event => {
|
||||||
|
if (nowMoving && rolledNumber) {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const rect = canvas.getBoundingClientRect(),
|
||||||
|
x = event.clientX - rect.left,
|
||||||
|
y = event.clientY - rect.top;
|
||||||
|
canvas.style.cursor = 'default';
|
||||||
|
for (const pawn of pawns) {
|
||||||
|
if (pawn.touchableArea) {
|
||||||
|
if (
|
||||||
|
ctx.isPointInPath(pawn.touchableArea, x, y) &&
|
||||||
|
player.color === pawn.color &&
|
||||||
|
canPawnMove(pawn, rolledNumber)
|
||||||
|
) {
|
||||||
|
const pawnPosition = getPositionAfterMove(pawn, rolledNumber);
|
||||||
|
if (pawnPosition) {
|
||||||
|
canvas.style.cursor = 'pointer';
|
||||||
|
setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHintPawn(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHintPawn(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHintPawn(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const rerenderCanvas = () => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const image = new Image();
|
||||||
|
image.src = mapImage;
|
||||||
|
image.onload = function () {
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
pawns.forEach((pawn, index) => {
|
||||||
|
pawns[index].touchableArea = paintPawn(
|
||||||
|
ctx,
|
||||||
|
positions[pawn.position].x,
|
||||||
|
positions[pawn.position].y,
|
||||||
|
pawn.color
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (hintPawn) {
|
||||||
|
paintPawn(ctx, positions[hintPawn.position].x, positions[hintPawn.position].y, hintPawn.color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
rerenderCanvas();
|
||||||
|
}, [hintPawn, pawns]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className='canvas-container'
|
||||||
|
width={460}
|
||||||
|
height={460}
|
||||||
|
ref={canvasRef}
|
||||||
|
onClick={handleCanvasClick}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Map;
|
||||||
27
src/components/Gameboard/Map/canPawnMove.js
Normal file
27
src/components/Gameboard/Map/canPawnMove.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const canPawnMove = (pawn, rolledNumber) => {
|
||||||
|
// If is in base
|
||||||
|
if ((rolledNumber === 1 || 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) {
|
||||||
|
switch (pawn.color) {
|
||||||
|
case 'red':
|
||||||
|
if (pawn.position + rolledNumber <= 73) return true;
|
||||||
|
break;
|
||||||
|
case 'blue':
|
||||||
|
if (pawn.position + rolledNumber <= 79) return true;
|
||||||
|
break;
|
||||||
|
case 'green':
|
||||||
|
if (pawn.position + rolledNumber <= 85) return true;
|
||||||
|
break;
|
||||||
|
case 'yellow':
|
||||||
|
if (pawn.position + rolledNumber <= 91) return true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default canPawnMove;
|
||||||
@ -1,19 +1,4 @@
|
|||||||
const { colors } = require('./constants');
|
const getPositionAfterMove = (pawn, rolledNumber) => {
|
||||||
function getStartPositions() {
|
|
||||||
const startPositions = [];
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
let pawn = {};
|
|
||||||
pawn.basePos = i;
|
|
||||||
pawn.position = i;
|
|
||||||
if (i < 4) pawn.color = colors[0];
|
|
||||||
else if (i < 8) pawn.color = colors[1];
|
|
||||||
else if (i < 12) pawn.color = colors[2];
|
|
||||||
else if (i < 16) pawn.color = colors[3];
|
|
||||||
startPositions.push(pawn);
|
|
||||||
}
|
|
||||||
return startPositions;
|
|
||||||
}
|
|
||||||
function getPawnPositionAfterMove(rolledNumber, pawn) {
|
|
||||||
const { position, color } = pawn;
|
const { position, color } = pawn;
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case 'red':
|
case 'red':
|
||||||
@ -70,6 +55,9 @@ function getPawnPositionAfterMove(rolledNumber, pawn) {
|
|||||||
} else {
|
} else {
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
module.exports = { getStartPositions, getPawnPositionAfterMove };
|
|
||||||
|
export default getPositionAfterMove;
|
||||||
35
src/components/LoginPage/AddServer/AddServer.css
Normal file
35
src/components/LoginPage/AddServer/AddServer.css
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.refresh {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
.refresh > img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-container {
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
input:disabled {
|
||||||
|
background-color: black;
|
||||||
|
color: #999;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
57
src/components/LoginPage/AddServer/AddServer.jsx
Normal file
57
src/components/LoginPage/AddServer/AddServer.jsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState, useContext, useEffect } from 'react';
|
||||||
|
import './AddServer.css';
|
||||||
|
import Switch from '@mui/material/Switch';
|
||||||
|
import { SocketContext } from '../../../App';
|
||||||
|
const AddServer = () => {
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
const [isPrivate, setIsPrivate] = useState(false);
|
||||||
|
const [serverName, setServerName] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on('room:created', () => {
|
||||||
|
socket.emit('room:rooms');
|
||||||
|
});
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const handleButtonClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
socket.emit('room:create', {
|
||||||
|
name: serverName,
|
||||||
|
private: isPrivate,
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='lp-container'>
|
||||||
|
<div className='title-container'>
|
||||||
|
<h1>Host A Server</h1>
|
||||||
|
</div>
|
||||||
|
<div className='content-container'>
|
||||||
|
<form>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={serverName}
|
||||||
|
onChange={e => setServerName(e.target.value)}
|
||||||
|
placeholder='Server Name'
|
||||||
|
/>
|
||||||
|
<div className='private-container'>
|
||||||
|
<p>Private</p>
|
||||||
|
<Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
placeholder='password'
|
||||||
|
disabled={!isPrivate}
|
||||||
|
/>
|
||||||
|
<button onClick={handleButtonClick}>Host</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddServer;
|
||||||
53
src/components/LoginPage/LoginPage.css
Normal file
53
src/components/LoginPage/LoginPage.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.login-page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: 50%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lp-container {
|
||||||
|
margin: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 500px;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 2px;
|
||||||
|
transform: scaleX(1.02);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
padding-left: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container > h1 {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
15
src/components/LoginPage/LoginPage.jsx
Normal file
15
src/components/LoginPage/LoginPage.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import './LoginPage.css';
|
||||||
|
import AddServer from './AddServer/AddServer';
|
||||||
|
import ServerList from './ServerList/ServerList';
|
||||||
|
const LoginPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='login-page-container'>
|
||||||
|
<ServerList />
|
||||||
|
<AddServer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
70
src/components/LoginPage/NameInput/NameInput.css
Normal file
70
src/components/LoginPage/NameInput/NameInput.css
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
.name-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 20px 60px 20px;
|
||||||
|
width: 300px;
|
||||||
|
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.name-input-container > button {
|
||||||
|
margin-top: 5px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.name-input-container > input {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.name-overlay {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
color: black;
|
||||||
|
border-color: #4a90e2;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 1);
|
||||||
|
}
|
||||||
51
src/components/LoginPage/NameInput/NameInput.jsx
Normal file
51
src/components/LoginPage/NameInput/NameInput.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React, { useState, useContext, useEffect, useCallback } from 'react';
|
||||||
|
import { SocketContext } from '../../../App';
|
||||||
|
import useInput from '../../../hooks/useInput';
|
||||||
|
import './NameInput.css';
|
||||||
|
import Overlay from '../../Overlay/Overlay';
|
||||||
|
|
||||||
|
const NameInput = ({ isRoomPrivate, roomId }) => {
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
const nickname = useInput('');
|
||||||
|
const password = useInput('');
|
||||||
|
const [isPasswordWrong, setIsPasswordWrong] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = useCallback(() => {
|
||||||
|
socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId });
|
||||||
|
}, [socket, nickname.value, password.value, roomId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on('error:wrongPassword', () => {
|
||||||
|
setIsPasswordWrong(true);
|
||||||
|
});
|
||||||
|
const keyDownHandler = event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleButtonClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', keyDownHandler);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', keyDownHandler);
|
||||||
|
};
|
||||||
|
}, [socket, handleButtonClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='name-overlay'>
|
||||||
|
<div className='name-input-container' style={{ height: isRoomPrivate ? '100px' : '50px' }}>
|
||||||
|
<input placeholder='Nickname' type='text' onChange={nickname.onChange} />
|
||||||
|
{isRoomPrivate ? (
|
||||||
|
<input
|
||||||
|
placeholder='Room password'
|
||||||
|
type='text'
|
||||||
|
onChange={password.onChange}
|
||||||
|
style={{ backgroundColor: isPasswordWrong ? 'red' : null }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<button onClick={handleButtonClick}>JOIN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NameInput;
|
||||||
51
src/components/LoginPage/ServerList/ServerList.css
Normal file
51
src/components/LoginPage/ServerList/ServerList.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
margin-right: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
max-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.server-container {
|
||||||
|
display: flex;
|
||||||
|
height: 500px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
.room-name {
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
/* Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: auto;
|
||||||
|
scrollbar-color: #ffffff rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome, Edge, and Safari */
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
77
src/components/LoginPage/ServerList/ServerList.jsx
Normal file
77
src/components/LoginPage/ServerList/ServerList.jsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { SocketContext } from '../../../App';
|
||||||
|
import lock from '../../../images/login-page/lock.png';
|
||||||
|
import refresh from '../../../images/login-page/refresh.png';
|
||||||
|
import ReactLoading from 'react-loading';
|
||||||
|
|
||||||
|
import './ServerList.css';
|
||||||
|
import NameInput from '../NameInput/NameInput';
|
||||||
|
|
||||||
|
const ServerList = () => {
|
||||||
|
const socket = useContext(SocketContext);
|
||||||
|
const [rooms, setRooms] = useState([]);
|
||||||
|
const [joining, setJoining] = useState(false);
|
||||||
|
const [clickedRoom, setClickedRoom] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
socket.emit('room:rooms');
|
||||||
|
socket.on('room:rooms', data => {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
setRooms(data);
|
||||||
|
});
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const getRooms = () => {
|
||||||
|
setRooms(null);
|
||||||
|
socket.emit('room:rooms');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoinClick = room => {
|
||||||
|
setClickedRoom(room);
|
||||||
|
setJoining(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='lp-container'>
|
||||||
|
<div className='title-container'>
|
||||||
|
<h1>Server List</h1>
|
||||||
|
<div className='refresh'>
|
||||||
|
<img src={refresh} alt='refresh' onClick={getRooms}></img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='server-container content-container'>
|
||||||
|
{rooms ? (
|
||||||
|
<table className='rooms'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Server</th>
|
||||||
|
<th>#/#</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rooms.map((room, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||||
|
<td className='room-name'>{room.name}</td>
|
||||||
|
<td>{`${room.players.length}/4`}</td>
|
||||||
|
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
||||||
|
<td>
|
||||||
|
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<div style={{ alignSelf: 'center' }}>
|
||||||
|
<ReactLoading type='spinningBubbles' color='white' height={50} width={50} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{joining ? <NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ServerList;
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import React, { useState, useContext } from "react";
|
|
||||||
import { SocketContext } from "../App";
|
|
||||||
|
|
||||||
const NameInput = () => {
|
|
||||||
const socket = useContext(SocketContext);
|
|
||||||
const [inputValue, setInputValue] = useState("");
|
|
||||||
const handleInputChange = (e) => {
|
|
||||||
setInputValue(e.target.value);
|
|
||||||
};
|
|
||||||
const handleButtonClick = () => {
|
|
||||||
socket.emit("player:login", { name: inputValue });
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
placeholder="Enter name"
|
|
||||||
type="text"
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<input type="submit" onClick={handleButtonClick} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NameInput;
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
.red {
|
|
||||||
position: relative;
|
|
||||||
left: 176px;
|
|
||||||
}
|
|
||||||
.yellow {
|
|
||||||
position: relative;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
right: 170px;
|
|
||||||
}
|
|
||||||
.blue {
|
|
||||||
position: relative;
|
|
||||||
right: 28px;
|
|
||||||
top: 538px;
|
|
||||||
}
|
|
||||||
.green {
|
|
||||||
position: relative;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
top: 538px;
|
|
||||||
left: 36px;
|
|
||||||
}
|
|
||||||
.player-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.dice-container {
|
|
||||||
margin-left: 20px;
|
|
||||||
margin-right: 20px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
.roll {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { CSSTransition } from 'react-transition-group';
|
||||||
|
import './TimerAnimation.js';
|
||||||
|
|
||||||
|
const AnimatedOverlay = ({ time }) => {
|
||||||
|
const animationDelay = useMemo(() => 15 - Math.ceil((time - Date.now()) / 1000), [time]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CSSTransition
|
||||||
|
in={true}
|
||||||
|
timeout={0}
|
||||||
|
classNames='overlay'
|
||||||
|
style={{ animationDelay: `-${animationDelay}s` }}
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<div className='overlay'></div>
|
||||||
|
</CSSTransition>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimatedOverlay;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
const keyframes = [];
|
||||||
|
const steps = 86;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
let s = 'polygon(50% 50%, 50% 0%, 50% 0%';
|
||||||
|
|
||||||
|
for (let i = 50; i <= 100; i += 5) {
|
||||||
|
s += `, ${i}% 0%`;
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
for (let i = 0; i <= 100; i += 5) {
|
||||||
|
s += `, 100% ${i}%`;
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
for (let i = 100; i >= 0; i -= 5) {
|
||||||
|
s += `, ${i}% 100%`;
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 100; i >= 0; i -= 5) {
|
||||||
|
s += `, 0% ${i}%`;
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
for (let i = 0; i <= 50; i += 5) {
|
||||||
|
s += `, ${i}% 0%`;
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle() {
|
||||||
|
const percentage = (count / steps) * 100;
|
||||||
|
let step;
|
||||||
|
if (percentage <= 75 && percentage >= 73) {
|
||||||
|
step = `${percentage}% {
|
||||||
|
background-color: orange;
|
||||||
|
clip-path: ${s})
|
||||||
|
}`;
|
||||||
|
} else if (percentage > 97.5 && percentage < 100) {
|
||||||
|
step = `${percentage}% {
|
||||||
|
background-color: red;
|
||||||
|
clip-path: ${s})
|
||||||
|
}`;
|
||||||
|
} else if (percentage > 0 && percentage < 2.5) {
|
||||||
|
step = `${percentage}% {
|
||||||
|
background-color: green;
|
||||||
|
clip-path: ${s})
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
step = `${percentage}% {
|
||||||
|
clip-path: ${s})
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
keyframes.push(step);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.styleSheets[0].insertRule(
|
||||||
|
`
|
||||||
|
@keyframes timerAnimation {
|
||||||
|
${keyframes.join('\n')}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
document.styleSheets[0].cssRules.length
|
||||||
|
);
|
||||||
22
src/components/Navbar/NameContainer/NameContainer.jsx
Normal file
22
src/components/Navbar/NameContainer/NameContainer.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import AnimatedOverlay from './AnimatedOverlay/AnimatedOverlay';
|
||||||
|
|
||||||
|
const NameContainer = ({ player, time }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='name-container'
|
||||||
|
style={player.ready ? { backgroundColor: player.color } : { backgroundColor: 'lightgrey' }}
|
||||||
|
>
|
||||||
|
<p>{player.name}</p>
|
||||||
|
{player.nowMoving ? <AnimatedOverlay time={time} /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NameContainer.propTypes = {
|
||||||
|
player: PropTypes.object,
|
||||||
|
time: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NameContainer;
|
||||||
55
src/components/Navbar/Navbar.css
Normal file
55
src/components/Navbar/Navbar.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
.dice-container {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
.roll {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ready-container {
|
||||||
|
display: flex;
|
||||||
|
width: 300px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
background-color: grey;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid white;
|
||||||
|
}
|
||||||
|
.ready-container > label {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 100px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.player-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
.yellow {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
margin-top: 50px;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 4;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
margin-top: 50px;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 4;
|
||||||
|
}
|
||||||
|
/* Styl dla overlay */
|
||||||
@ -1,13 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Dice from './game-board-components/Dice';
|
import Dice from '../Gameboard/Dice/Dice';
|
||||||
import NameContainer from './navbar-components/NameContainer';
|
import NameContainer from './NameContainer/NameContainer';
|
||||||
import ReadyButton from './navbar-components/ReadyButton';
|
import ReadyButton from './ReadyButton/ReadyButton';
|
||||||
import './Navbar.css';
|
import './Navbar.css';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { PlayerDataContext } from '../../App';
|
||||||
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => {
|
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => {
|
||||||
|
const context = useContext(PlayerDataContext);
|
||||||
const colors = ['red', 'blue', 'green', 'yellow'];
|
const colors = ['red', 'blue', 'green', 'yellow'];
|
||||||
return (
|
return (
|
||||||
<div className='navbar-container'>
|
<>
|
||||||
{players.map((player, index) => (
|
{players.map((player, index) => (
|
||||||
<div className={`player-container ${colors[index]}`} key={index}>
|
<div className={`player-container ${colors[index]}`} key={index}>
|
||||||
<NameContainer player={player} time={time} />
|
<NameContainer player={player} time={time} />
|
||||||
@ -18,10 +20,10 @@ const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, roll
|
|||||||
color={colors[index]}
|
color={colors[index]}
|
||||||
rolledNumberCallback={rolledNumberCallback}
|
rolledNumberCallback={rolledNumberCallback}
|
||||||
/>
|
/>
|
||||||
|
{context.color !== player.color || started ? null : <ReadyButton isReady={isReady} />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{started ? null : <ReadyButton isReady={isReady} />}
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Navbar;
|
export default Navbar;
|
||||||
@ -1,17 +1,17 @@
|
|||||||
import React, { useState, useContext, useEffect } from 'react';
|
import React, { useState, useContext } from 'react';
|
||||||
import { SocketContext } from '../../App';
|
import { SocketContext } from '../../../App';
|
||||||
import Switch from '@material-ui/core/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
|
import '../Navbar.css';
|
||||||
|
import '../NameContainer/AnimatedOverlay/TimerAnimation';
|
||||||
|
|
||||||
const ReadyButton = ({ isReady }) => {
|
const ReadyButton = ({ isReady }) => {
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
const [checked, setChecked] = useState();
|
const [checked, setChecked] = useState(isReady);
|
||||||
|
|
||||||
const handleCheckboxChange = () => {
|
const handleCheckboxChange = () => {
|
||||||
socket.emit('player:ready');
|
socket.emit('player:ready');
|
||||||
setChecked(!checked);
|
setChecked(!checked);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
|
||||||
setChecked(isReady);
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className='ready-container'>
|
<div className='ready-container'>
|
||||||
<Switch onChange={handleCheckboxChange} checked={checked || false} />
|
<Switch onChange={handleCheckboxChange} checked={checked || false} />
|
||||||
@ -1,230 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState, useContext, useCallback } from 'react';
|
|
||||||
import { PlayerDataContext, SocketContext } from '../../App';
|
|
||||||
import positions from './positions';
|
|
||||||
import bluePawn from '../../images/pawns/blue-pawn.png';
|
|
||||||
import greenPawn from '../../images/pawns/green-pawn.png';
|
|
||||||
import yellowPawn from '../../images/pawns/yellow-pawn.png';
|
|
||||||
import redPawn from '../../images/pawns/red-pawn.png';
|
|
||||||
import greyPawn from '../../images/pawns/grey-pawn.png';
|
|
||||||
const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
|
||||||
const context = useContext(PlayerDataContext);
|
|
||||||
const socket = useContext(SocketContext);
|
|
||||||
const [hintPawn, setHintPawn] = useState();
|
|
||||||
const paintPawn = (context, x, y, color) => {
|
|
||||||
const circle = new Path2D();
|
|
||||||
circle.arc(x, y, 12, 0, 2 * Math.PI);
|
|
||||||
const image = new Image();
|
|
||||||
switch (color) {
|
|
||||||
case 'green':
|
|
||||||
image.src = greenPawn;
|
|
||||||
break;
|
|
||||||
case 'blue':
|
|
||||||
image.src = bluePawn;
|
|
||||||
break;
|
|
||||||
case 'red':
|
|
||||||
image.src = redPawn;
|
|
||||||
break;
|
|
||||||
case 'yellow':
|
|
||||||
image.src = yellowPawn;
|
|
||||||
break;
|
|
||||||
case 'grey':
|
|
||||||
image.src = greyPawn;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
context.drawImage(image, x - 17, y - 14, 35, 30);
|
|
||||||
return circle;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canvasRef = useRef(null);
|
|
||||||
|
|
||||||
// Return true when pawn can move
|
|
||||||
const checkIfPawnCanMove = useCallback(
|
|
||||||
pawn => {
|
|
||||||
// If is in base
|
|
||||||
if ((rolledNumber === 1 || 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) {
|
|
||||||
switch (pawn.color) {
|
|
||||||
case 'red':
|
|
||||||
if (pawn.position + rolledNumber <= 73) return true;
|
|
||||||
break;
|
|
||||||
case 'blue':
|
|
||||||
if (pawn.position + rolledNumber <= 79) return true;
|
|
||||||
break;
|
|
||||||
case 'green':
|
|
||||||
if (pawn.position + rolledNumber <= 85) return true;
|
|
||||||
break;
|
|
||||||
case 'yellow':
|
|
||||||
if (pawn.position + rolledNumber <= 91) return true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[rolledNumber]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCanvasClick = event => {
|
|
||||||
// If hint pawn exist it means that pawn can move
|
|
||||||
if (hintPawn) {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const rect = canvas.getBoundingClientRect(),
|
|
||||||
x = event.clientX - rect.left,
|
|
||||||
y = event.clientY - rect.top;
|
|
||||||
for (const pawn of pawns) {
|
|
||||||
if (ctx.isPointInPath(pawn.circle, x, y)) {
|
|
||||||
socket.emit('game:move', pawn._id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setHintPawn(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getHintPawnPosition = pawn => {
|
|
||||||
// Based on color (because specific color have specific base and end positions)
|
|
||||||
let { position } = pawn;
|
|
||||||
switch (context.color) {
|
|
||||||
case 'red':
|
|
||||||
// When in base
|
|
||||||
if (position >= 0 && position <= 3) {
|
|
||||||
return 16;
|
|
||||||
// Next to end
|
|
||||||
} else if (position <= 66 && position + rolledNumber >= 67) {
|
|
||||||
return position + rolledNumber + 1; // 1 is difference between last position on map and first on end
|
|
||||||
// Normal move
|
|
||||||
} else {
|
|
||||||
return position + rolledNumber;
|
|
||||||
}
|
|
||||||
case 'blue':
|
|
||||||
// When in base
|
|
||||||
if (position >= 4 && position <= 7) {
|
|
||||||
return 55;
|
|
||||||
// Next to red base
|
|
||||||
} else if (position <= 67 && position + rolledNumber > 67) {
|
|
||||||
return position + rolledNumber - 52;
|
|
||||||
// Next to base
|
|
||||||
} else if (position <= 53 && position + rolledNumber >= 54) {
|
|
||||||
return position + rolledNumber + 20;
|
|
||||||
// Normal move
|
|
||||||
} else {
|
|
||||||
return position + rolledNumber;
|
|
||||||
}
|
|
||||||
case 'green':
|
|
||||||
// When in base
|
|
||||||
if (position >= 8 && position <= 11) {
|
|
||||||
return 42;
|
|
||||||
// Next to red base
|
|
||||||
} else if (position <= 67 && position + rolledNumber > 67) {
|
|
||||||
return position + rolledNumber - 52;
|
|
||||||
// Next to base
|
|
||||||
} else if (position <= 40 && position + rolledNumber >= 41) {
|
|
||||||
return position + rolledNumber + 39;
|
|
||||||
// Normal move
|
|
||||||
} else {
|
|
||||||
return position + rolledNumber;
|
|
||||||
}
|
|
||||||
case 'yellow':
|
|
||||||
// When in base
|
|
||||||
if (position >= 12 && position <= 15) {
|
|
||||||
return 29;
|
|
||||||
// Next to red base
|
|
||||||
} else if (position <= 67 && position + rolledNumber > 67) {
|
|
||||||
return position + rolledNumber - 52;
|
|
||||||
// Next to base
|
|
||||||
} else if (position <= 27 && position + rolledNumber >= 28) {
|
|
||||||
return position + rolledNumber + 58;
|
|
||||||
// Normal move
|
|
||||||
} else {
|
|
||||||
return position + rolledNumber;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleMouseMove = event => {
|
|
||||||
if (nowMoving && rolledNumber) {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
// Gets x and y cords of mouse on canvas
|
|
||||||
const rect = canvas.getBoundingClientRect(),
|
|
||||||
x = event.clientX - rect.left,
|
|
||||||
y = event.clientY - rect.top;
|
|
||||||
canvas.style.cursor = 'default';
|
|
||||||
for (const pawn of pawns) {
|
|
||||||
if (pawn.circle) {
|
|
||||||
/*
|
|
||||||
This condition checks if mouse location is:
|
|
||||||
1) on pawn
|
|
||||||
2) is color of pawn same as player's
|
|
||||||
3) if pawn can move
|
|
||||||
And then sets cursor to pointer and paints hint pawn - where will be pawn after click
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
ctx.isPointInPath(pawn.circle, x, y) &&
|
|
||||||
context.color === pawn.color &&
|
|
||||||
checkIfPawnCanMove(pawn)
|
|
||||||
) {
|
|
||||||
const pawnPosition = getHintPawnPosition(pawn);
|
|
||||||
// Checks if pawn can make a move
|
|
||||||
if (pawnPosition) {
|
|
||||||
canvas.style.cursor = 'pointer';
|
|
||||||
setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHintPawn(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const rerenderCanvas = useCallback(() => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const image = new Image();
|
|
||||||
image.src = 'https://img-9gag-fun.9cache.com/photo/a8GdpYZ_460s.jpg';
|
|
||||||
image.onload = function () {
|
|
||||||
ctx.drawImage(image, 0, 0);
|
|
||||||
pawns.forEach((pawn, index) => {
|
|
||||||
pawns[index].circle = paintPawn(
|
|
||||||
ctx,
|
|
||||||
positions[pawn.position].x,
|
|
||||||
positions[pawn.position].y,
|
|
||||||
pawn.color
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (hintPawn) {
|
|
||||||
paintPawn(ctx, positions[hintPawn.position].x, positions[hintPawn.position].y, hintPawn.color);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [checkIfPawnCanMove, context.color, hintPawn, nowMoving, pawns, rolledNumber]);
|
|
||||||
|
|
||||||
// Rerender canvas when pawns have changed
|
|
||||||
useEffect(() => {
|
|
||||||
rerenderCanvas();
|
|
||||||
}, [hintPawn, pawns, rerenderCanvas]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
socket.on('game:move', () => {
|
|
||||||
setHintPawn(null);
|
|
||||||
});
|
|
||||||
socket.on('game:roll', () => {
|
|
||||||
setHintPawn(null);
|
|
||||||
});
|
|
||||||
}, [socket]);
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
className='canvas-container'
|
|
||||||
width={460}
|
|
||||||
height={460}
|
|
||||||
ref={canvasRef}
|
|
||||||
onClick={handleCanvasClick}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Map;
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { SocketContext } from '../../App';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Component responsible for:
|
|
||||||
- displaying the player's name
|
|
||||||
- informing players about the readiness of other players by changing the color of container from gray to the player's color
|
|
||||||
- counting time to the end of the move
|
|
||||||
|
|
||||||
Props:
|
|
||||||
- player (object):
|
|
||||||
The player to whom the container belongs
|
|
||||||
Player's properties used in this component:
|
|
||||||
- ready (boolean):
|
|
||||||
is the player ready for the start of the game, if so, change color from gray to the player's color
|
|
||||||
when the game is started all players are ready not matter if they clicked ready button before
|
|
||||||
- nowMoving (boolean) is this player move now, if true display timer
|
|
||||||
- name (string)
|
|
||||||
- time (number) - time remaining until the move is made in milliseconds
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NameContainer = ({ player, time }) => {
|
|
||||||
const [remainingTime, setRemainingTime] = useState();
|
|
||||||
const socket = useContext(SocketContext);
|
|
||||||
|
|
||||||
// Function responsible for counting down to the end of time every second
|
|
||||||
const countdown = () => {
|
|
||||||
setRemainingTime(Math.ceil((time - Date.now()) / 1000));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Starts the countdown from the beginning if the server returned information about skipping the turn
|
|
||||||
socket.on('game:skip', () => {
|
|
||||||
setRemainingTime(15);
|
|
||||||
});
|
|
||||||
setRemainingTime(Math.ceil((time - Date.now()) / 1000));
|
|
||||||
const interval = setInterval(countdown, 1000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [countdown]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='name-container'
|
|
||||||
style={player.ready ? { backgroundColor: player.color } : { backgroundColor: 'lightgrey' }}
|
|
||||||
>
|
|
||||||
<p>{player.name}</p>
|
|
||||||
{player.nowMoving ? <div className='timer'> {remainingTime} </div> : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
NameContainer.propTypes = {
|
|
||||||
player: PropTypes.object,
|
|
||||||
time: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NameContainer;
|
|
||||||
15
src/constants/pawnImages.js
Normal file
15
src/constants/pawnImages.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import bluePawn from '../images/pawns/blue-pawn.png';
|
||||||
|
import greenPawn from '../images/pawns/green-pawn.png';
|
||||||
|
import redPawn from '../images/pawns/red-pawn.png';
|
||||||
|
import yellowPawn from '../images/pawns/yellow-pawn.png';
|
||||||
|
import greyPawn from '../images/pawns/grey-pawn.png';
|
||||||
|
|
||||||
|
const pawnImages = {
|
||||||
|
green: greenPawn,
|
||||||
|
blue: bluePawn,
|
||||||
|
red: redPawn,
|
||||||
|
yellow: yellowPawn,
|
||||||
|
grey: greyPawn,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pawnImages;
|
||||||
117
src/constants/positions.js
Normal file
117
src/constants/positions.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
const positions = [
|
||||||
|
// Red base
|
||||||
|
{ x: 67, y: 67 }, // 0
|
||||||
|
{ x: 67, y: 116 },
|
||||||
|
{ x: 117, y: 67 },
|
||||||
|
{ x: 117, y: 116 },
|
||||||
|
// Blue base
|
||||||
|
{ x: 67, y: 343 },
|
||||||
|
{ x: 67, y: 392 },
|
||||||
|
{ x: 117, y: 343 },
|
||||||
|
{ x: 117, y: 392 },
|
||||||
|
// Green base
|
||||||
|
{ x: 343, y: 343 },
|
||||||
|
{ x: 392, y: 392 },
|
||||||
|
{ x: 392, y: 343 }, // 10
|
||||||
|
{ x: 343, y: 392 },
|
||||||
|
// Yellow base
|
||||||
|
{ x: 343, y: 67 },
|
||||||
|
{ x: 392, y: 116 },
|
||||||
|
{ x: 392, y: 67 },
|
||||||
|
{ x: 343, y: 116 },
|
||||||
|
// Map - starting from red field
|
||||||
|
{ x: 45, y: 200 },
|
||||||
|
{ x: 76, y: 200 },
|
||||||
|
{ x: 107, y: 200 },
|
||||||
|
{ x: 138, y: 200 },
|
||||||
|
{ x: 169, y: 200 }, // 20
|
||||||
|
|
||||||
|
{ x: 200, y: 169 },
|
||||||
|
{ x: 200, y: 138 },
|
||||||
|
{ x: 200, y: 107 },
|
||||||
|
{ x: 200, y: 76 },
|
||||||
|
{ x: 200, y: 45 },
|
||||||
|
{ x: 200, y: 14 },
|
||||||
|
// Top
|
||||||
|
{ x: 230, y: 14 },
|
||||||
|
{ x: 261, y: 14 },
|
||||||
|
{ x: 261, y: 45 },
|
||||||
|
{ x: 261, y: 76 }, // 30
|
||||||
|
{ x: 261, y: 107 },
|
||||||
|
{ x: 261, y: 138 },
|
||||||
|
{ x: 261, y: 169 },
|
||||||
|
|
||||||
|
{ x: 291, y: 200 },
|
||||||
|
{ x: 321, y: 200 },
|
||||||
|
{ x: 352, y: 200 },
|
||||||
|
{ x: 383, y: 200 },
|
||||||
|
{ x: 414, y: 200 },
|
||||||
|
{ x: 445, y: 200 },
|
||||||
|
// Right
|
||||||
|
{ x: 445, y: 230 }, // 40
|
||||||
|
|
||||||
|
{ x: 445, y: 261 },
|
||||||
|
{ x: 414, y: 261 },
|
||||||
|
{ x: 383, y: 261 },
|
||||||
|
{ x: 352, y: 261 },
|
||||||
|
{ x: 321, y: 261 },
|
||||||
|
{ x: 291, y: 261 },
|
||||||
|
|
||||||
|
{ x: 261, y: 291 },
|
||||||
|
{ x: 261, y: 322 },
|
||||||
|
{ x: 261, y: 353 },
|
||||||
|
{ x: 261, y: 384 }, // 50
|
||||||
|
{ x: 261, y: 414 },
|
||||||
|
{ x: 261, y: 445 },
|
||||||
|
// Bottom
|
||||||
|
{ x: 230, y: 445 },
|
||||||
|
|
||||||
|
{ x: 200, y: 445 },
|
||||||
|
{ x: 200, y: 414 },
|
||||||
|
{ x: 200, y: 384 },
|
||||||
|
{ x: 200, y: 353 },
|
||||||
|
{ x: 200, y: 322 },
|
||||||
|
{ x: 200, y: 291 },
|
||||||
|
|
||||||
|
{ x: 169, y: 261 }, // 60
|
||||||
|
{ x: 138, y: 261 },
|
||||||
|
{ x: 107, y: 261 },
|
||||||
|
{ x: 76, y: 261 },
|
||||||
|
{ x: 45, y: 261 },
|
||||||
|
|
||||||
|
{ x: 15, y: 261 },
|
||||||
|
// Left
|
||||||
|
{ x: 15, y: 231 }, // 66
|
||||||
|
// One behind red base
|
||||||
|
{ x: 15, y: 200 }, //67
|
||||||
|
// Red end
|
||||||
|
{ x: 45, y: 231 }, // 68
|
||||||
|
{ x: 76, y: 231 },
|
||||||
|
{ x: 107, y: 231 },
|
||||||
|
{ x: 138, y: 231 },
|
||||||
|
{ x: 169, y: 231 },
|
||||||
|
{ x: 200, y: 231 }, // 73
|
||||||
|
// Blue end
|
||||||
|
{ x: 231, y: 414 }, // 74
|
||||||
|
{ x: 231, y: 384 },
|
||||||
|
{ x: 231, y: 353 },
|
||||||
|
{ x: 231, y: 322 },
|
||||||
|
{ x: 231, y: 291 },
|
||||||
|
{ x: 231, y: 260 }, // 79
|
||||||
|
// Green end
|
||||||
|
{ x: 414, y: 231 }, // 80
|
||||||
|
{ x: 383, y: 231 },
|
||||||
|
{ x: 352, y: 231 },
|
||||||
|
{ x: 321, y: 231 },
|
||||||
|
{ x: 290, y: 231 },
|
||||||
|
{ x: 259, y: 231 }, // 85
|
||||||
|
// Yellow base
|
||||||
|
{ x: 230, y: 45 }, // 86
|
||||||
|
{ x: 230, y: 76 },
|
||||||
|
{ x: 230, y: 107 },
|
||||||
|
{ x: 230, y: 138 },
|
||||||
|
{ x: 230, y: 169 },
|
||||||
|
{ x: 230, y: 200 }, // 91
|
||||||
|
];
|
||||||
|
|
||||||
|
export default positions;
|
||||||
11
src/hooks/useInput.js
Normal file
11
src/hooks/useInput.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
export default function useInput({ initialValue }) {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const handleChange = e => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
onChange: handleChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
BIN
src/images/login-page/lock.png
Normal file
BIN
src/images/login-page/lock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
src/images/login-page/refresh.png
Normal file
BIN
src/images/login-page/refresh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/images/login-page/user.png
Normal file
BIN
src/images/login-page/user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
BIN
src/images/map.jpg
Normal file
BIN
src/images/map.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 408 KiB |
@ -6,13 +6,16 @@ body {
|
|||||||
rgba(0, 138, 255, 1) 16%,
|
rgba(0, 138, 255, 1) 16%,
|
||||||
rgba(9, 9, 121, 1) 81%
|
rgba(9, 9, 121, 1) 81%
|
||||||
);
|
);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#root {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
height: 100vh;
|
||||||
}
|
width: 100vw;
|
||||||
.canvas-container {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
@ -25,12 +28,14 @@ canvas {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.navbar-container > div {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.name-container {
|
.name-container {
|
||||||
width: 100px;
|
position: relative;
|
||||||
height: 50px;
|
min-width: 100px;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: white;
|
color: white;
|
||||||
@ -49,9 +54,33 @@ canvas {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
.overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: timerAnimation 15s linear infinite;
|
||||||
|
transition-duration: 15s;
|
||||||
|
}
|
||||||
#root {
|
#root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
grid-template-columns: 230px 230px;
|
||||||
|
grid-template-rows: 50px 250px 250px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
place-self: center;
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
grid-row: 2 / span 2;
|
||||||
|
}
|
||||||
|
|||||||
11
src/index.js
11
src/index.js
@ -1,11 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
ReactDOM.render(
|
const container = document.getElementById('root');
|
||||||
<React.StrictMode>
|
const root = ReactDOM.createRoot(container);
|
||||||
<App />
|
root.render(<App />);
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user