diff --git a/backend/config/database.js b/backend/config/database.js new file mode 100644 index 0000000..5901cec --- /dev/null +++ b/backend/config/database.js @@ -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)); +}; diff --git a/backend/controllers/serverController.js b/backend/config/session.js similarity index 100% rename from backend/controllers/serverController.js rename to backend/config/session.js diff --git a/backend/config/socket.js b/backend/config/socket.js new file mode 100644 index 0000000..feac406 --- /dev/null +++ b/backend/config/socket.js @@ -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)); + } + }); +}; diff --git a/backend/controllers/roomController.js b/backend/controllers/roomController.js new file mode 100644 index 0000000..f698a3e --- /dev/null +++ b/backend/controllers/roomController.js @@ -0,0 +1,35 @@ +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 = () => { + const room = new Room(); + return room; +}; + +const findPlayer = async sessionID => { + const player = await Room.findOne({ 'players.sessionID': sessionID }).exec(); + console.log(player); + return await Room.findOne({ 'players.sessionID': sessionID }).exec(); +}; + +Room.watch().on('change', async data => { + sendToPlayersData(await getRoom(data.documentKey._id)); +}); + +module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom, findPlayer }; diff --git a/backend/handlers/gameHandler.js b/backend/handlers/gameHandler.js index b4f244c..a13ff04 100644 --- a/backend/handlers/gameHandler.js +++ b/backend/handlers/gameHandler.js @@ -1,88 +1,34 @@ -const Room = require('../schemas/room'); +const { getRoom, updateRoom } = require('../controllers/roomController'); +const { sendToPlayersRolledNumber } = require('../socket/emits'); const { getPawnPositionAfterMove } = require('../utils/functions'); +const { rollDice, isMoveValid } = require('./handlersFunctions'); -module.exports = (io, socket) => { +module.exports = socket => { const req = socket.request; const handleMovePawn = async pawnId => { - const room = await getRoom(); + const room = await getRoom(req.session.roomId); const pawn = room.getPawn(pawnId); - if (isMoveValid(pawn, room)) { + if (isMoveValid(req.session, pawn, room)) { const newPositionOfMovedPawn = getPawnPositionAfterMove(room.rolledNumber, pawn); room.changePositionOfPawn(pawn, newPositionOfMovedPawn); room.beatPawns(newPositionOfMovedPawn, req.session.color); - handleChangeOfPlayer(room); + room.changeMovingPlayer(); + await updateRoom(room); } }; const handleRollDice = async () => { const rolledNumber = rollDice(); - const room = await updateRoom({ rolledNumber: rolledNumber }); - if (!canPlayerMove(room, rolledNumber)) { - handleChangeOfPlayer(room); + sendToPlayersRolledNumber(req.session.roomId, rolledNumber); + const room = await updateRoom({ _id: req.session.roomId, rolledNumber: rolledNumber }); + 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:move', handleMovePawn); }; diff --git a/backend/handlers/handlersFunctions.js b/backend/handlers/handlersFunctions.js new file mode 100644 index 0000000..a457d54 --- /dev/null +++ b/backend/handlers/handlersFunctions.js @@ -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 }; diff --git a/backend/handlers/playerHandler.js b/backend/handlers/playerHandler.js index 2f178ff..2918d92 100644 --- a/backend/handlers/playerHandler.js +++ b/backend/handlers/playerHandler.js @@ -1,32 +1,31 @@ -const RoomModel = require('../schemas/room'); +const { getRoom, updateRoom, getJoinableRoom, createNewRoom, findPlayer } = require('../controllers/roomController'); const { colors } = require('../utils/constants'); -module.exports = (io, socket) => { +module.exports = socket => { const req = socket.request; const handleLogin = async data => { - const room = await RoomModel.findOne({ full: false, started: false }); + if (await findPlayer(req.sessionID)) return; + const room = await getJoinableRoom(); if (room) { addPlayerToExistingRoom(room, data); } else { - createNewRoom(data); + addNewRoom(data); } }; const handleReady = async () => { - const { roomId, playerId } = req.session; - const room = await RoomModel.findOne({ _id: roomId }); - room.getPlayer(playerId).changeReadyStatus(); + const room = await getRoom(req.session.roomId); + room.getPlayer(req.session.playerId).changeReadyStatus(); if (room.canStartGame()) { room.startGame(); } - await RoomModel.findOneAndUpdate({ _id: roomId }, room); - io.to(roomId).emit('room:data', JSON.stringify(room)); + await updateRoom(room); }; - const createNewRoom = async data => { - const room = new RoomModel(); - room.addPlayer(data.name); + const addNewRoom = async data => { + const room = createNewRoom(); + room.addPlayer(data.name, req.sessionID); await room.save(); reloadSession(room); }; @@ -36,7 +35,7 @@ module.exports = (io, socket) => { if (room.isFull()) { room.startGame(); } - await RoomModel.findOneAndUpdate({ _id: room._id }, room); + await updateRoom(room); reloadSession(room); }; diff --git a/backend/handlers/roomHandler.js b/backend/handlers/roomHandler.js index 5630cc0..fbde0d6 100644 --- a/backend/handlers/roomHandler.js +++ b/backend/handlers/roomHandler.js @@ -1,39 +1,37 @@ -const RoomModel = require('../schemas/room'); +const { getRooms, getRoom, updateRoom } = require('../controllers/roomController'); +const { sendToOnePlayerRooms, sendToOnePlayerData, sendToPlayersData } = require('../socket/emits'); -module.exports = (io, socket) => { +module.exports = socket => { const req = socket.request; const getData = async () => { - let room = await RoomModel.findOne({ _id: req.session.roomId }); + const room = await getRoom(req.session.roomId); // Handle the situation when the server crashes and any player reconnects after the time has expired // Typically, the responsibility for changing players is managed by gameHandler.js. if (room.nextMoveTime <= Date.now()) { room.changeMovingPlayer(); - await RoomModel.findOneAndUpdate({ _id: req.session.roomId }, room); - io.to(req.session.roomId).emit('room:data', JSON.stringify(room)); - } else { - io.to(socket.id).emit('room:data', JSON.stringify(room)); + await updateRoom(room); } + sendToOnePlayerData(socket.id, room); }; - const getRooms = async () => { - let rooms = await RoomModel.find({}); + const getAllRooms = async () => { + let rooms = await getRooms(); const response = []; rooms.forEach(room => { if (!room.isStarted && !room.isFull()) { response.push({ _id: room._id, + private: room.private, name: room.name, players: room.players, isStarted: room.isStarted, }); } }); - io.to(socket.id).emit('room:rooms', JSON.stringify(response)); + sendToOnePlayerRooms(socket.id, response); }; - - socket.on('room:data', getData); - socket.on('room:rooms', getRooms); + socket.on('room:rooms', getAllRooms); }; diff --git a/backend/schemas/pawn.js b/backend/models/pawn.js similarity index 100% rename from backend/schemas/pawn.js rename to backend/models/pawn.js diff --git a/backend/schemas/player.js b/backend/models/player.js similarity index 58% rename from backend/schemas/player.js rename to backend/models/player.js index fe507a5..bad4ee4 100644 --- a/backend/schemas/player.js +++ b/backend/models/player.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; const PlayerSchema = new Schema({ + sessionID: String, name: String, color: String, ready: { type: Boolean, default: false }, @@ -13,4 +14,12 @@ 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; diff --git a/backend/schemas/room.js b/backend/models/room.js similarity index 87% rename from backend/schemas/room.js rename to backend/models/room.js index 90379b6..c1672d4 100644 --- a/backend/schemas/room.js +++ b/backend/models/room.js @@ -1,12 +1,14 @@ const mongoose = require('mongoose'); const { colors } = require('../utils/constants'); const { getPawnPositionAfterMove, getStartPositions } = require('../utils/functions'); -const Schema = mongoose.Schema; +const { makeRandomMove } = require('../handlers/handlersFunctions'); const PawnSchema = require('./pawn'); const PlayerSchema = require('./player'); -const RoomSchema = new Schema({ +const RoomSchema = new mongoose.Schema({ name: String, + private: { type: Boolean, default: true }, + password: String, createDate: { type: Date, default: Date.now }, started: { type: Boolean, default: false }, full: { type: Boolean, default: false }, @@ -38,7 +40,7 @@ RoomSchema.methods.changeMovingPlayer = function () { this.nextMoveTime = Date.now() + 15000; this.rolledNumber = null; if (this.timeoutID) clearTimeout(this.timeoutID); - this.timeoutID = null; + this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString()); }; RoomSchema.methods.movePawn = function (pawn) { @@ -67,7 +69,7 @@ RoomSchema.methods.startGame = function () { this.nextMoveTime = Date.now() + 15000; this.players.forEach(player => (player.ready = true)); this.players[0].nowMoving = true; - this.timeoutID = setTimeout(makeRandomMove, 15000, this); + this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString()); }; RoomSchema.methods.isFull = function () { @@ -81,9 +83,10 @@ RoomSchema.methods.getPlayer = function (playerId) { return this.players.find(player => player._id.toString() === playerId.toString()); }; -RoomSchema.methods.addPlayer = function (name) { +RoomSchema.methods.addPlayer = function (name, id) { if (this.full) return; this.players.push({ + sessionID: id, name: name, ready: false, color: colors[this.players.length], @@ -106,6 +109,6 @@ RoomSchema.methods.getCurrentlyMovingPlayer = function () { 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; diff --git a/backend/server.js b/backend/server.js index 21341f1..f018038 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,13 +1,11 @@ const express = require('express'); const cors = require('cors'); 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 CONNECTION_URI = require('./credentials.js'); +const { sessionMiddleware } = require('./config/session'); + +const PORT = 8080; + const app = express(); app.use(cookieParser()); @@ -26,63 +24,10 @@ app.use( ); app.use(sessionMiddleware); -mongoose.set('useFindAndModify', false); -mongoose - .connect(CONNECTION_URI, { - useNewUrlParser: true, - useUnifiedTopology: true, - }) - .then(() => { - console.log('MongoDB Connected…'); - }) - .catch(err => console.error(err)); +const server = app.listen(PORT); -const server = app.listen(PORT, () => { - console.log('Server runs on port ' + PORT); -}); - -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)); - } -}); +require('./config/database')(mongoose); +require('./config/socket')(server); if (process.env.NODE_ENV === 'production') { app.use(express.static('/app/build')); diff --git a/backend/socket/emits.js b/backend/socket/emits.js new file mode 100644 index 0000000..e0d6eb7 --- /dev/null +++ b/backend/socket/emits.js @@ -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 }; diff --git a/backend/socket/socketManager.js b/backend/socket/socketManager.js new file mode 100644 index 0000000..d6bca63 --- /dev/null +++ b/backend/socket/socketManager.js @@ -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;