From ffaaf0b6f34224cf81493768d7eb730aed8690c2 Mon Sep 17 00:00:00 2001 From: Wenszel Date: Thu, 23 Nov 2023 21:20:44 +0100 Subject: [PATCH 1/4] added LoginPage --- backend/handlers/roomHandler.js | 20 +++++ backend/schemas/room.js | 2 + backend/server.js | 2 +- src/App.js | 10 ++- src/components/LoginPage/LoginPage.css | 76 ++++++++++++++++++ src/components/LoginPage/LoginPage.jsx | 54 +++++++++++++ .../LoginPage/NameInput/NameInput.css | 47 +++++++++++ .../LoginPage/NameInput/NameInput.jsx | 21 +++++ src/components/NameInput.jsx | 25 ------ src/images/login-page/user.png | Bin 0 -> 16826 bytes 10 files changed, 228 insertions(+), 29 deletions(-) create mode 100644 src/components/LoginPage/LoginPage.css create mode 100644 src/components/LoginPage/LoginPage.jsx create mode 100644 src/components/LoginPage/NameInput/NameInput.css create mode 100644 src/components/LoginPage/NameInput/NameInput.jsx delete mode 100644 src/components/NameInput.jsx create mode 100644 src/images/login-page/user.png diff --git a/backend/handlers/roomHandler.js b/backend/handlers/roomHandler.js index bfdeef3..5630cc0 100644 --- a/backend/handlers/roomHandler.js +++ b/backend/handlers/roomHandler.js @@ -15,5 +15,25 @@ module.exports = (io, socket) => { io.to(socket.id).emit('room:data', JSON.stringify(room)); } }; + + const getRooms = async () => { + let rooms = await RoomModel.find({}); + const response = []; + rooms.forEach(room => { + if (!room.isStarted && !room.isFull()) { + response.push({ + _id: room._id, + name: room.name, + players: room.players, + isStarted: room.isStarted, + }); + } + }); + io.to(socket.id).emit('room:rooms', JSON.stringify(response)); + }; + + + socket.on('room:data', getData); + socket.on('room:rooms', getRooms); }; diff --git a/backend/schemas/room.js b/backend/schemas/room.js index e171950..90379b6 100644 --- a/backend/schemas/room.js +++ b/backend/schemas/room.js @@ -6,6 +6,7 @@ const PawnSchema = require('./pawn'); const PlayerSchema = require('./player'); const RoomSchema = new Schema({ + name: String, createDate: { type: Date, default: Date.now }, started: { type: Boolean, default: false }, full: { type: Boolean, default: false }, @@ -66,6 +67,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); }; RoomSchema.methods.isFull = function () { diff --git a/backend/server.js b/backend/server.js index fb85acb..21341f1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -91,4 +91,4 @@ if (process.env.NODE_ENV === 'production') { }); } -module.exports = { server }; \ No newline at end of file +module.exports = { server }; diff --git a/src/App.js b/src/App.js index fa208b4..770998b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,9 @@ import React, { useEffect, useState, createContext } from 'react'; import { io } from 'socket.io-client'; import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; - +import ReactLoading from 'react-loading'; import Gameboard from './components/Gameboard'; -import NameInput from './components/NameInput'; +import LoginPage from './components/LoginPage/LoginPage'; export const PlayerDataContext = createContext(); export const SocketContext = createContext(); @@ -32,7 +32,11 @@ function App() { LOADING... - + {playerSocket ? ( + + ) : ( + + )} {playerData ? ( diff --git a/src/components/LoginPage/LoginPage.css b/src/components/LoginPage/LoginPage.css new file mode 100644 index 0000000..62099aa --- /dev/null +++ b/src/components/LoginPage/LoginPage.css @@ -0,0 +1,76 @@ +.login-page-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50vh; + width: 400px; + position: relative; + padding: 20px; + border-radius: 5%; + border: 5px solid white; + background-color: rgba(0, 0, 0, 0.5); +} +h1 { + margin-right: 10px; + align-self: flex-start; + top: 0; + position: absolute; + color: white; +} +.rooms { + width: 98%; + height: 80%; + overflow-y: scroll; + overflow-x: hidden; +} +.room { + cursor: pointer; + justify-content: space-between; + display: flex; + flex-direction: row; + align-items: center; + color: white; + width: 90%; + margin: 10px; + padding: 10px; + border: 1px solid black; +} +.room-selected { + border: 1px solid white; +} +.room-selected, +.room:hover { + background-color: rgba(0, 0, 0, 0.5); +} +.number-of-players { + display: flex; + flex-direction: row; + align-items: center; +} +.number-of-players > img { + margin-right: 5px; + width: 20px; + height: 20px; +} + +/* Firefox */ +* { + scrollbar-width: auto; + scrollbar-color: #ffffff rgba(0, 0, 0, 0.1); +} + +/* Chrome, Edge, and Safari */ +*::-webkit-scrollbar { + width: 8px; +} + +*::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0); +} + +*::-webkit-scrollbar-thumb { + background-color: #ffffff; + border-radius: 10px; + border: 3px none #ffffff; +} diff --git a/src/components/LoginPage/LoginPage.jsx b/src/components/LoginPage/LoginPage.jsx new file mode 100644 index 0000000..835787d --- /dev/null +++ b/src/components/LoginPage/LoginPage.jsx @@ -0,0 +1,54 @@ +import React, { useContext, useEffect, useState } from 'react'; +import NameInput from './NameInput/NameInput'; +import { SocketContext } from '../../App'; +import './LoginPage.css'; +import userImage from '../../images/login-page/user.png'; +const LoginPage = () => { + const socket = useContext(SocketContext); + const [rooms, setRooms] = useState([]); + const [selectedRoom, setSelectedRoom] = useState(null); + + useEffect(async () => { + socket.emit('room:rooms'); + socket.on('room:rooms', data => { + data = JSON.parse(data); + console.log(data); + setRooms(data); + }); + }, []); + + return ( +
+

Select room:

+
+ {rooms.map(room => { + return ( +
{ + if (selectedRoom && selectedRoom == room._id) { + setSelectedRoom(null); + } else { + setSelectedRoom(room._id); + } + }} + key={room.id} + > +
+

{room.name}

+ {room.players.map(player => player.name + ' ')} +
+ +
+ + {room.players.length}/4 +
+
+ ); + })} +
+ +
+ ); +}; +export default LoginPage; diff --git a/src/components/LoginPage/NameInput/NameInput.css b/src/components/LoginPage/NameInput/NameInput.css new file mode 100644 index 0000000..f7509fe --- /dev/null +++ b/src/components/LoginPage/NameInput/NameInput.css @@ -0,0 +1,47 @@ +.name-input-container { + display: flex; + position: absolute; + bottom: 0; + flex-direction: row; + width: 80%; + margin: 20px; +} +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.5); + 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.5); + cursor: pointer; + transition: background-color 0.3s ease-in-out; +} + +button:hover { + background-color: rgba(0, 0, 0, 1); +} diff --git a/src/components/LoginPage/NameInput/NameInput.jsx b/src/components/LoginPage/NameInput/NameInput.jsx new file mode 100644 index 0000000..491c56b --- /dev/null +++ b/src/components/LoginPage/NameInput/NameInput.jsx @@ -0,0 +1,21 @@ +import React, { useState, useContext } from 'react'; +import { SocketContext } from '../../../App'; +import './NameInput.css'; +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 ( +
+ + +
+ ); +}; + +export default NameInput; diff --git a/src/components/NameInput.jsx b/src/components/NameInput.jsx deleted file mode 100644 index 9f4d1eb..0000000 --- a/src/components/NameInput.jsx +++ /dev/null @@ -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 ( -
- - -
- ); -}; - -export default NameInput; diff --git a/src/images/login-page/user.png b/src/images/login-page/user.png new file mode 100644 index 0000000000000000000000000000000000000000..6d15e9e62526ea987387257509b31beb06e18793 GIT binary patch literal 16826 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

IA?)MjP!%=gz=kK*5%-P zGP#1@`MIw2dKAE5{sQ*`Gu3Z^?saPH8Fz3u#F+xH^>W%~&nrF9(1iL9XzmhsJDC9P zp{aQx$p?`!_jzcoe|$vO@42Z#5JLre*3zyT-vmqG15848sc~L0DpmdeC=uH+8T)}uOL7~j&bD)?_NzY>ynF-H zCm*my$vl|aY+dxpYU>{L)J*tGJ#Di?mA+_DJ7JE;R0Q3ooaS|%7?r0&_N#HC8Ap>@ z$%r#{BLKs7&PROx-b3D=_yADuPOjS?4J1IlfGb%Unaf$MHig3Hpi=9$M-b`t{Bvji zOOLUgkU_BH+s&>obnYFtp_bli6!{!AUR7<9pX_CROyI@!HGo>&`uOd&+80@Y6V zpFXW)mAd{#tW??Vv0igEj=-JD>#SKs3~HE}TWC0LlU(vGvbf3|;*hf>Uu?V5seFy2 zFm4$msHsbwSaE!i*+-Z0!9+o;hnkH&ECVFnzKo#yjz^S&RF1|HMt(xx8kZAEkEBUL z`nTzkpzoX6mdKK&T~k2C2)Q3|w@vcX*Vi`F-)V$`q`n6;$1Sow<>pMeSGkq+7wTHc znIeodOh~D-He$!PTvm#;af5J$;Zh5H6fh+TiEC~DM^?kmxA~a(3nk(sxp7qv;*@ep z-JURPIiXy&pzG97z`nUG($I=V)^RWBTcMb`(+^Wrqo6TR=oKne`j?ea*Ux9Ybt0>1 zNQ#(**2~R(3nrP+q|}4b9cUhbd#0muARa7@M%H+x;MPRi4D`^NyUCFV{rgVoXEHMT z2CuzZOK~^+U41Q8{DD@=&HWqLu#f1N)|OC3T-O}}uNI6R%h>>M8dWupYUKW~Yewgp z;;gb73`K=FO5(b~p!x|m>+Oqpw#3yxq5(+*Oj)dWtfV_{L=w23x_-@VO##FqMB*dU zhHAw^v|d_nV{7Y8b@A0b02l{ukXi_Z3Bus06wurk<`%d|O)au#g!bIDp{nnAR)ZSk zx(ojUdVg0+wwt9cy2RND{%+}Mzovj2Udx77e5ogFfzCp`FuZ)X3Y#&Oo#u^MKyE~{+@@Gbum$pmD) zorE|Dk!D_?L5HOsw94c*EBlS{^W*g1gih#uIv}0>d4|=>_MLcL{|m&!(hMUlZSF_- zY*PRTiM~dIZu~hO$iU*tft`s!R`p5R$y#3(&9x z?wkS?L9$FKZVFSg!;z?!l<6d{gUx}WK6WhpNGH(v82j!i2~G)m1DsE*(%W1(lqPA? z@V3bb%^=V&Ge}5hms%1)2vK{?pCbzGpO_ObIRxg#f<~^1OzAsL8{7leyi90qm75YR zJ`VETEb(`FW9WiM;KtGvZfd(Xg3Zi+-^!L2M4Yk3O>4;#X8?L42q@428D&9lQlnQ> z*C~()%c0KLq1O+_9t22F^3o-WsNn}v%Fom;*%V%yk}60GxeYQKVY-QfDy zldElDnHw@~g6vj+r@0&ZH@HuIqvT0Y-9GfblXP!t?T?*@t0HvOzvjo+J%WBfp^H Date: Sat, 25 Nov 2023 19:30:07 +0100 Subject: [PATCH 2/4] refactored backend --- backend/config/database.js | 14 ++++ .../serverController.js => config/session.js} | 0 backend/config/socket.js | 26 ++++++ backend/controllers/roomController.js | 35 ++++++++ backend/handlers/gameHandler.js | 82 ++++--------------- backend/handlers/handlersFunctions.js | 35 ++++++++ backend/handlers/playerHandler.js | 25 +++--- backend/handlers/roomHandler.js | 24 +++--- backend/{schemas => models}/pawn.js | 0 backend/{schemas => models}/player.js | 9 ++ backend/{schemas => models}/room.js | 17 ++-- backend/server.js | 69 ++-------------- backend/socket/emits.js | 19 +++++ backend/socket/socketManager.js | 39 +++++++++ 14 files changed, 231 insertions(+), 163 deletions(-) create mode 100644 backend/config/database.js rename backend/{controllers/serverController.js => config/session.js} (100%) create mode 100644 backend/config/socket.js create mode 100644 backend/controllers/roomController.js create mode 100644 backend/handlers/handlersFunctions.js rename backend/{schemas => models}/pawn.js (100%) rename backend/{schemas => models}/player.js (58%) rename backend/{schemas => models}/room.js (87%) create mode 100644 backend/socket/emits.js create mode 100644 backend/socket/socketManager.js 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; From f318afe071fd9cea4ec980aca8dd0ec44803dc45 Mon Sep 17 00:00:00 2001 From: Wenszel Date: Tue, 28 Nov 2023 18:51:50 +0100 Subject: [PATCH 3/4] added joining to room --- backend/controllers/roomController.js | 5 +- backend/handlers/playerHandler.js | 21 ++-- backend/handlers/roomHandler.js | 30 +++--- backend/models/room.js | 2 +- .../LoginPage/AddServer/AddServer.css | 35 +++++++ .../LoginPage/AddServer/AddServer.jsx | 58 +++++++++++ src/components/LoginPage/LoginPage.css | 97 +++++++----------- src/components/LoginPage/LoginPage.jsx | 56 ++-------- .../LoginPage/NameInput/NameInput.css | 35 +++++-- .../LoginPage/NameInput/NameInput.jsx | 45 ++++++-- .../LoginPage/ServerList/ServerList.css | 51 +++++++++ .../LoginPage/ServerList/ServerList.jsx | 79 ++++++++++++++ .../navbar-components/AnimatedOverlay.jsx | 2 +- src/images/login-page/lock.png | Bin 0 -> 10430 bytes src/images/login-page/refresh.png | Bin 0 -> 22493 bytes src/images/login-page/user.png | Bin 16826 -> 9069 bytes 16 files changed, 356 insertions(+), 160 deletions(-) create mode 100644 src/components/LoginPage/AddServer/AddServer.css create mode 100644 src/components/LoginPage/AddServer/AddServer.jsx create mode 100644 src/components/LoginPage/ServerList/ServerList.css create mode 100644 src/components/LoginPage/ServerList/ServerList.jsx create mode 100644 src/images/login-page/lock.png create mode 100644 src/images/login-page/refresh.png diff --git a/backend/controllers/roomController.js b/backend/controllers/roomController.js index f698a3e..80a36ac 100644 --- a/backend/controllers/roomController.js +++ b/backend/controllers/roomController.js @@ -17,8 +17,9 @@ const getJoinableRoom = async () => { return await Room.findOne({ full: false, started: false }).exec(); }; -const createNewRoom = () => { - const room = new Room(); +const createNewRoom = data => { + const room = new Room(data); + room.save(); return room; }; diff --git a/backend/handlers/playerHandler.js b/backend/handlers/playerHandler.js index 2918d92..825d568 100644 --- a/backend/handlers/playerHandler.js +++ b/backend/handlers/playerHandler.js @@ -1,17 +1,15 @@ -const { getRoom, updateRoom, getJoinableRoom, createNewRoom, findPlayer } = require('../controllers/roomController'); +const { getRoom, updateRoom } = require('../controllers/roomController'); const { colors } = require('../utils/constants'); module.exports = socket => { const req = socket.request; const handleLogin = async data => { - if (await findPlayer(req.sessionID)) return; - const room = await getJoinableRoom(); - if (room) { - addPlayerToExistingRoom(room, data); - } else { - addNewRoom(data); - } + const room = await getRoom(data.roomId); + if (room.isFull()) return socket.emit('error:changeRoom'); + if (room.started) return socket.emit('error:changeRoom'); + if (room.private && room.password !== data.password) return socket.emit('error:wrongPassword'); + addPlayerToExistingRoom(room, data); }; const handleReady = async () => { @@ -23,13 +21,6 @@ module.exports = socket => { await updateRoom(room); }; - const addNewRoom = async data => { - const room = createNewRoom(); - room.addPlayer(data.name, req.sessionID); - await room.save(); - reloadSession(room); - }; - const addPlayerToExistingRoom = async (room, data) => { room.addPlayer(data.name); if (room.isFull()) { diff --git a/backend/handlers/roomHandler.js b/backend/handlers/roomHandler.js index fbde0d6..bee90de 100644 --- a/backend/handlers/roomHandler.js +++ b/backend/handlers/roomHandler.js @@ -1,10 +1,10 @@ -const { getRooms, getRoom, updateRoom } = require('../controllers/roomController'); +const { getRooms, getRoom, updateRoom, createNewRoom } = require('../controllers/roomController'); const { sendToOnePlayerRooms, sendToOnePlayerData, sendToPlayersData } = require('../socket/emits'); module.exports = socket => { const req = socket.request; - const getData = async () => { + const handleGetData = async () => { 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. @@ -15,23 +15,17 @@ module.exports = socket => { sendToOnePlayerData(socket.id, room); }; - const getAllRooms = async () => { + const handleGetAllRooms = 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, - }); - } - }); - sendToOnePlayerRooms(socket.id, response); + sendToOnePlayerRooms(socket.id, rooms); }; - socket.on('room:data', getData); - socket.on('room:rooms', getAllRooms); + const handleCreateRoom = async data => { + createNewRoom(data); + socket.to(socket.id).emit('room:created'); + }; + + socket.on('room:data', handleGetData); + socket.on('room:rooms', handleGetAllRooms); + socket.on('room:create', handleCreateRoom); }; diff --git a/backend/models/room.js b/backend/models/room.js index c1672d4..2c7ca34 100644 --- a/backend/models/room.js +++ b/backend/models/room.js @@ -7,7 +7,7 @@ const PlayerSchema = require('./player'); const RoomSchema = new mongoose.Schema({ name: String, - private: { type: Boolean, default: true }, + private: { type: Boolean, default: false }, password: String, createDate: { type: Date, default: Date.now }, started: { type: Boolean, default: false }, diff --git a/src/components/LoginPage/AddServer/AddServer.css b/src/components/LoginPage/AddServer/AddServer.css new file mode 100644 index 0000000..4c64921 --- /dev/null +++ b/src/components/LoginPage/AddServer/AddServer.css @@ -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; +} diff --git a/src/components/LoginPage/AddServer/AddServer.jsx b/src/components/LoginPage/AddServer/AddServer.jsx new file mode 100644 index 0000000..054bf54 --- /dev/null +++ b/src/components/LoginPage/AddServer/AddServer.jsx @@ -0,0 +1,58 @@ +import React, { useState, useContext, useEffect } from 'react'; +import './AddServer.css'; +import Switch from '@material-ui/core/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', () => { + console.log('ewa'); + socket.emit('room:rooms'); + }); + }, []); + + const handleButtonClick = e => { + e.preventDefault(); + socket.emit('room:create', { + name: serverName, + private: isPrivate, + password: password, + }); + }; + + return ( +

+
+

Host A Server

+
+
+
+ setServerName(e.target.value)} + placeholder='Server Name' + /> +
+

Private

+ setIsPrivate(!isPrivate)} /> +
+ setPassword(e.target.value)} + placeholder='password' + disabled={!isPrivate} + /> + +
+
+
+ ); +}; + +export default AddServer; diff --git a/src/components/LoginPage/LoginPage.css b/src/components/LoginPage/LoginPage.css index 62099aa..3376c6f 100644 --- a/src/components/LoginPage/LoginPage.css +++ b/src/components/LoginPage/LoginPage.css @@ -1,76 +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; - height: 50vh; - width: 400px; - position: relative; + width: 500px; padding: 20px; - border-radius: 5%; - border: 5px solid white; - background-color: rgba(0, 0, 0, 0.5); -} -h1 { - margin-right: 10px; - align-self: flex-start; - top: 0; - position: absolute; color: white; } -.rooms { - width: 98%; - height: 80%; - overflow-y: scroll; - overflow-x: hidden; -} -.room { - cursor: pointer; - justify-content: space-between; + +.title-container { display: flex; flex-direction: row; + justify-content: center; align-items: center; - color: white; - width: 90%; - margin: 10px; - padding: 10px; - border: 1px solid black; -} -.room-selected { + 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; } -.room-selected, -.room:hover { - background-color: rgba(0, 0, 0, 0.5); + +.title-container > h1 { + width: 100%; + margin: 0; + padding: 0; } -.number-of-players { + +.content-container { display: flex; - flex-direction: row; - align-items: center; -} -.number-of-players > img { - margin-right: 5px; - width: 20px; - height: 20px; -} - -/* Firefox */ -* { - scrollbar-width: auto; - scrollbar-color: #ffffff rgba(0, 0, 0, 0.1); -} - -/* Chrome, Edge, and Safari */ -*::-webkit-scrollbar { - width: 8px; -} - -*::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0); -} - -*::-webkit-scrollbar-thumb { - background-color: #ffffff; - border-radius: 10px; - border: 3px none #ffffff; + 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; } diff --git a/src/components/LoginPage/LoginPage.jsx b/src/components/LoginPage/LoginPage.jsx index 835787d..9532e2d 100644 --- a/src/components/LoginPage/LoginPage.jsx +++ b/src/components/LoginPage/LoginPage.jsx @@ -1,54 +1,16 @@ -import React, { useContext, useEffect, useState } from 'react'; -import NameInput from './NameInput/NameInput'; -import { SocketContext } from '../../App'; import './LoginPage.css'; -import userImage from '../../images/login-page/user.png'; +import AddServer from './AddServer/AddServer'; +import ServerList from './ServerList/ServerList'; +import NameInput from './NameInput/NameInput'; const LoginPage = () => { - const socket = useContext(SocketContext); - const [rooms, setRooms] = useState([]); - const [selectedRoom, setSelectedRoom] = useState(null); - - useEffect(async () => { - socket.emit('room:rooms'); - socket.on('room:rooms', data => { - data = JSON.parse(data); - console.log(data); - setRooms(data); - }); - }, []); - return ( -
-

Select room:

-
- {rooms.map(room => { - return ( -
{ - if (selectedRoom && selectedRoom == room._id) { - setSelectedRoom(null); - } else { - setSelectedRoom(room._id); - } - }} - key={room.id} - > -
-

{room.name}

- {room.players.map(player => player.name + ' ')} -
- -
- - {room.players.length}/4 -
-
- ); - })} + <> +
+ +
- -
+ ); }; + export default LoginPage; diff --git a/src/components/LoginPage/NameInput/NameInput.css b/src/components/LoginPage/NameInput/NameInput.css index f7509fe..9f6282c 100644 --- a/src/components/LoginPage/NameInput/NameInput.css +++ b/src/components/LoginPage/NameInput/NameInput.css @@ -1,11 +1,34 @@ .name-input-container { display: flex; - position: absolute; - bottom: 0; - flex-direction: row; - width: 80%; + 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; @@ -21,7 +44,7 @@ input { border-radius: 8px; color: white; border: 1px solid #ccc; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.2); transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out; } @@ -37,7 +60,7 @@ button { border-radius: 8px; border: none; color: #fff; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.4); cursor: pointer; transition: background-color 0.3s ease-in-out; } diff --git a/src/components/LoginPage/NameInput/NameInput.jsx b/src/components/LoginPage/NameInput/NameInput.jsx index 491c56b..e1212e4 100644 --- a/src/components/LoginPage/NameInput/NameInput.jsx +++ b/src/components/LoginPage/NameInput/NameInput.jsx @@ -1,19 +1,44 @@ -import React, { useState, useContext } from 'react'; +import React, { useState, useContext, useEffect } from 'react'; import { SocketContext } from '../../../App'; import './NameInput.css'; -const NameInput = () => { +const NameInput = ({ isRoomPrivate, roomId }) => { const socket = useContext(SocketContext); - const [inputValue, setInputValue] = useState(''); - const handleInputChange = e => { - setInputValue(e.target.value); - }; + const [nickname, setNickname] = useState(''); + const [password, setPassword] = useState(''); + const [isPasswordWrong, setIsPasswordWrong] = useState(false); const handleButtonClick = () => { - socket.emit('player:login', { name: inputValue }); + socket.emit('player:login', { name: nickname, password: password, roomId: 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); + }; + }, []); + return ( -
- - +
+
+ setNickname(e.target.value)} /> + {isRoomPrivate ? ( + setPassword(e.target.value)} + style={{ backgroundColor: isPasswordWrong ? 'red' : null }} + /> + ) : null} + +
); }; diff --git a/src/components/LoginPage/ServerList/ServerList.css b/src/components/LoginPage/ServerList/ServerList.css new file mode 100644 index 0000000..318a008 --- /dev/null +++ b/src/components/LoginPage/ServerList/ServerList.css @@ -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; +} diff --git a/src/components/LoginPage/ServerList/ServerList.jsx b/src/components/LoginPage/ServerList/ServerList.jsx new file mode 100644 index 0000000..28e55a2 --- /dev/null +++ b/src/components/LoginPage/ServerList/ServerList.jsx @@ -0,0 +1,79 @@ +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(async () => { + socket.emit('room:rooms'); + socket.on('room:rooms', data => { + data = JSON.parse(data); + setRooms(data); + }); + }, []); + + const getRooms = () => { + setRooms(null); + socket.emit('room:rooms'); + }; + + const handleJoinClick = room => { + setClickedRoom(room); + setJoining(true); + }; + + return ( +
+
+

Server List

+
+ +
+
+
+ {rooms ? ( + + + + + + + + + + + + {rooms.map((room, index) => ( + + + + + + + + ))} + +
Server#/#Status
{room.private ? : null}{room.name}{`${room.players.length}/4`}{room.isStarted ? 'started' : 'waiting'} + +
+ ) : ( +
+ +
+ )} +
+ {joining ? ( + + ) : null} +
+ ); +}; +export default ServerList; diff --git a/src/components/navbar-components/AnimatedOverlay.jsx b/src/components/navbar-components/AnimatedOverlay.jsx index 82b1507..5fc82fa 100644 --- a/src/components/navbar-components/AnimatedOverlay.jsx +++ b/src/components/navbar-components/AnimatedOverlay.jsx @@ -14,7 +14,7 @@ const AnimatedOverlay = ({ time }) => { in={true} timeout={0} classNames='overlay' - style={{ 'animation-delay': `-${animationDelay}s` }} + style={{ animationDelay: `-${animationDelay}s` }} unmountOnExit >
diff --git a/src/images/login-page/lock.png b/src/images/login-page/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..8e56993856be6937ed7b3f36deab245f5f3fb5a9 GIT binary patch literal 10430 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 literal 0 HcmV?d00001 diff --git a/src/images/login-page/refresh.png b/src/images/login-page/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..25988e66a8ec795caa9d1f39f416c82083dd6e3c GIT binary patch literal 22493 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

IA?)MjP!%=gz=kK*5%-P zGP#1@`MIw2dKAE5{sQ*`Gu3Z^?saPH8Fz3u#F+xH^>W%~&nrF9(1iL9XzmhsJDC9P zp{aQx$p?`!_jzcoe|$vO@42Z#5JLre*3zyT-vmqG15848sc~L0DpmdeC=uH+8T)}uOL7~j&bD)?_NzY>ynF-H zCm*my$vl|aY+dxpYU>{L)J*tGJ#Di?mA+_DJ7JE;R0Q3ooaS|%7?r0&_N#HC8Ap>@ z$%r#{BLKs7&PROx-b3D=_yADuPOjS?4J1IlfGb%Unaf$MHig3Hpi=9$M-b`t{Bvji zOOLUgkU_BH+s&>obnYFtp_bli6!{!AUR7<9pX_CROyI@!HGo>&`uOd&+80@Y6V zpFXW)mAd{#tW??Vv0igEj=-JD>#SKs3~HE}TWC0LlU(vGvbf3|;*hf>Uu?V5seFy2 zFm4$msHsbwSaE!i*+-Z0!9+o;hnkH&ECVFnzKo#yjz^S&RF1|HMt(xx8kZAEkEBUL z`nTzkpzoX6mdKK&T~k2C2)Q3|w@vcX*Vi`F-)V$`q`n6;$1Sow<>pMeSGkq+7wTHc znIeodOh~D-He$!PTvm#;af5J$;Zh5H6fh+TiEC~DM^?kmxA~a(3nk(sxp7qv;*@ep z-JURPIiXy&pzG97z`nUG($I=V)^RWBTcMb`(+^Wrqo6TR=oKne`j?ea*Ux9Ybt0>1 zNQ#(**2~R(3nrP+q|}4b9cUhbd#0muARa7@M%H+x;MPRi4D`^NyUCFV{rgVoXEHMT z2CuzZOK~^+U41Q8{DD@=&HWqLu#f1N)|OC3T-O}}uNI6R%h>>M8dWupYUKW~Yewgp z;;gb73`K=FO5(b~p!x|m>+Oqpw#3yxq5(+*Oj)dWtfV_{L=w23x_-@VO##FqMB*dU zhHAw^v|d_nV{7Y8b@A0b02l{ukXi_Z3Bus06wurk<`%d|O)au#g!bIDp{nnAR)ZSk zx(ojUdVg0+wwt9cy2RND{%+}Mzovj2Udx77e5ogFfzCp`FuZ)X3Y#&Oo#u^MKyE~{+@@Gbum$pmD) zorE|Dk!D_?L5HOsw94c*EBlS{^W*g1gih#uIv}0>d4|=>_MLcL{|m&!(hMUlZSF_- zY*PRTiM~dIZu~hO$iU*tft`s!R`p5R$y#3(&9x z?wkS?L9$FKZVFSg!;z?!l<6d{gUx}WK6WhpNGH(v82j!i2~G)m1DsE*(%W1(lqPA? z@V3bb%^=V&Ge}5hms%1)2vK{?pCbzGpO_ObIRxg#f<~^1OzAsL8{7leyi90qm75YR zJ`VETEb(`FW9WiM;KtGvZfd(Xg3Zi+-^!L2M4Yk3O>4;#X8?L42q@428D&9lQlnQ> z*C~()%c0KLq1O+_9t22F^3o-WsNn}v%Fom;*%V%yk}60GxeYQKVY-QfDy zldElDnHw@~g6vj+r@0&ZH@HuIqvT0Y-9GfblXP!t?T?*@t0HvOzvjo+J%WBfp^H Date: Fri, 8 Dec 2023 17:25:51 +0100 Subject: [PATCH 4/4] refactored frontend --- backend/controllers/roomController.js | 8 +- backend/handlers/gameHandler.js | 3 +- backend/handlers/roomHandler.js | 2 +- backend/models/pawn.js | 64 ++++- backend/models/room.js | 21 +- src/App.js | 2 +- .../Dice}/Dice.jsx | 24 +- src/components/{ => Gameboard}/Gameboard.jsx | 22 +- src/components/Gameboard/Map/Map.jsx | 111 +++++++++ src/components/Gameboard/Map/canPawnMove.js | 26 ++ .../Gameboard/Map/getPositionAfterMove.js | 20 +- .../positions.js | 0 .../LoginPage/AddServer/AddServer.jsx | 1 - .../LoginPage/NameInput/NameInput.jsx | 11 +- .../LoginPage/ServerList/ServerList.jsx | 6 +- .../AnimatedOverlay}/AnimatedOverlay.jsx | 8 +- .../AnimatedOverlay}/TimerAnimation.js | 0 .../NameContainer}/NameContainer.jsx | 2 +- src/components/{ => Navbar}/Navbar.css | 0 src/components/{ => Navbar}/Navbar.jsx | 8 +- .../ReadyButton}/ReadyButton.jsx | 10 +- src/components/game-board-components/Map.jsx | 230 ------------------ src/constants/pawnImages.js | 13 + src/constants/positions.js | 117 +++++++++ src/hooks/useInput.js | 11 + src/images/map.jpg | Bin 0 -> 40140 bytes 26 files changed, 407 insertions(+), 313 deletions(-) rename src/components/{game-board-components => Gameboard/Dice}/Dice.jsx (62%) rename src/components/{ => Gameboard}/Gameboard.jsx (88%) create mode 100644 src/components/Gameboard/Map/Map.jsx create mode 100644 src/components/Gameboard/Map/canPawnMove.js rename backend/utils/functions.js => src/components/Gameboard/Map/getPositionAfterMove.js (79%) rename src/components/{game-board-components => Gameboard}/positions.js (100%) rename src/components/{navbar-components => Navbar/NameContainer/AnimatedOverlay}/AnimatedOverlay.jsx (67%) rename src/components/{navbar-components => Navbar/NameContainer/AnimatedOverlay}/TimerAnimation.js (100%) rename src/components/{navbar-components => Navbar/NameContainer}/NameContainer.jsx (89%) rename src/components/{ => Navbar}/Navbar.css (100%) rename src/components/{ => Navbar}/Navbar.jsx (82%) rename src/components/{navbar-components => Navbar/ReadyButton}/ReadyButton.jsx (77%) delete mode 100644 src/components/game-board-components/Map.jsx create mode 100644 src/constants/pawnImages.js create mode 100644 src/constants/positions.js create mode 100644 src/hooks/useInput.js create mode 100644 src/images/map.jpg diff --git a/backend/controllers/roomController.js b/backend/controllers/roomController.js index 80a36ac..8a5bf2a 100644 --- a/backend/controllers/roomController.js +++ b/backend/controllers/roomController.js @@ -23,14 +23,8 @@ const createNewRoom = data => { 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 }; +module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom }; diff --git a/backend/handlers/gameHandler.js b/backend/handlers/gameHandler.js index a13ff04..9ef9315 100644 --- a/backend/handlers/gameHandler.js +++ b/backend/handlers/gameHandler.js @@ -1,6 +1,5 @@ const { getRoom, updateRoom } = require('../controllers/roomController'); const { sendToPlayersRolledNumber } = require('../socket/emits'); -const { getPawnPositionAfterMove } = require('../utils/functions'); const { rollDice, isMoveValid } = require('./handlersFunctions'); module.exports = socket => { @@ -10,7 +9,7 @@ module.exports = socket => { const room = await getRoom(req.session.roomId); const pawn = room.getPawn(pawnId); if (isMoveValid(req.session, pawn, room)) { - const newPositionOfMovedPawn = getPawnPositionAfterMove(room.rolledNumber, pawn); + const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber); room.changePositionOfPawn(pawn, newPositionOfMovedPawn); room.beatPawns(newPositionOfMovedPawn, req.session.color); room.changeMovingPlayer(); diff --git a/backend/handlers/roomHandler.js b/backend/handlers/roomHandler.js index bee90de..f99324c 100644 --- a/backend/handlers/roomHandler.js +++ b/backend/handlers/roomHandler.js @@ -1,5 +1,5 @@ const { getRooms, getRoom, updateRoom, createNewRoom } = require('../controllers/roomController'); -const { sendToOnePlayerRooms, sendToOnePlayerData, sendToPlayersData } = require('../socket/emits'); +const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits'); module.exports = socket => { const req = socket.request; diff --git a/backend/models/pawn.js b/backend/models/pawn.js index 3df2429..2680cad 100644 --- a/backend/models/pawn.js +++ b/backend/models/pawn.js @@ -2,8 +2,6 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; -const { getPawnPositionAfterMove } = require('../utils/functions'); - const PawnSchema = new Schema({ color: String, basePos: Number, @@ -15,10 +13,70 @@ PawnSchema.methods.canMove = function (rolledNumber) { 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) { + 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; diff --git a/backend/models/room.js b/backend/models/room.js index 2c7ca34..8c3f25a 100644 --- a/backend/models/room.js +++ b/backend/models/room.js @@ -1,6 +1,5 @@ const mongoose = require('mongoose'); const { colors } = require('../utils/constants'); -const { getPawnPositionAfterMove, getStartPositions } = require('../utils/functions'); const { makeRandomMove } = require('../handlers/handlersFunctions'); const PawnSchema = require('./pawn'); const PlayerSchema = require('./player'); @@ -16,7 +15,23 @@ const RoomSchema = new mongoose.Schema({ timeoutID: Number, rolledNumber: Number, players: [PlayerSchema], - pawns: { type: [PawnSchema], default: getStartPositions() }, + 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) { @@ -44,7 +59,7 @@ RoomSchema.methods.changeMovingPlayer = function () { }; RoomSchema.methods.movePawn = function (pawn) { - const newPositionOfMovedPawn = getPawnPositionAfterMove(this.rolledNumber, pawn); + const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber); this.changePositionOfPawn(pawn, newPositionOfMovedPawn); this.beatPawns(newPositionOfMovedPawn, pawn.color); }; diff --git a/src/App.js b/src/App.js index 770998b..6853ef8 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,7 @@ import React, { useEffect, useState, createContext } from 'react'; import { io } from 'socket.io-client'; import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; import ReactLoading from 'react-loading'; -import Gameboard from './components/Gameboard'; +import Gameboard from './components/Gameboard/Gameboard'; import LoginPage from './components/LoginPage/LoginPage'; export const PlayerDataContext = createContext(); diff --git a/src/components/game-board-components/Dice.jsx b/src/components/Gameboard/Dice/Dice.jsx similarity index 62% rename from src/components/game-board-components/Dice.jsx rename to src/components/Gameboard/Dice/Dice.jsx index b18131e..98a0751 100644 --- a/src/components/game-board-components/Dice.jsx +++ b/src/components/Gameboard/Dice/Dice.jsx @@ -1,24 +1,28 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { SocketContext } from '../../App'; -import one from '../../images/dice/1.png'; -import two from '../../images/dice/2.png'; -import three from '../../images/dice/3.png'; -import four from '../../images/dice/4.png'; -import five from '../../images/dice/5.png'; -import six from '../../images/dice/6.png'; -import roll from '../../images/dice/roll.png'; +import React, { useEffect, useContext } from 'react'; +import { SocketContext } from '../../../App'; +import one from '../../../images/dice/1.png'; +import two from '../../../images/dice/2.png'; +import three from '../../../images/dice/3.png'; +import four from '../../../images/dice/4.png'; +import five from '../../../images/dice/5.png'; +import six from '../../../images/dice/6.png'; +import roll from '../../../images/dice/roll.png'; const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => { 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 = () => { socket.emit('game:roll'); }; + useEffect(() => { socket.on('game:roll', number => { rolledNumberCallback(number); }); }, []); + return (

{movingPlayer === color ? ( diff --git a/src/components/Gameboard.jsx b/src/components/Gameboard/Gameboard.jsx similarity index 88% rename from src/components/Gameboard.jsx rename to src/components/Gameboard/Gameboard.jsx index e0764d9..45f7ae8 100644 --- a/src/components/Gameboard.jsx +++ b/src/components/Gameboard/Gameboard.jsx @@ -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 { PlayerDataContext, SocketContext } from '../App'; -import Map from './game-board-components/Map'; -import Navbar from './Navbar'; +import { PlayerDataContext, SocketContext } from '../../App'; +import Map from './Map/Map'; +import Navbar from '../Navbar/Navbar'; const Gameboard = () => { - // Context data const socket = useContext(SocketContext); const context = useContext(PlayerDataContext); - // Render data + const [pawns, setPawns] = useState([]); const [players, setPlayers] = useState([]); - // Game logic data + const [rolledNumber, setRolledNumber] = useState(null); const [time, setTime] = useState(); const [isReady, setIsReady] = useState(); @@ -19,7 +18,8 @@ const Gameboard = () => { const [started, setStarted] = useState(false); const [movingPlayer, setMovingPlayer] = useState('red'); - const checkWin = useCallback(() => { + + const checkWin = () => { // 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'); @@ -30,7 +30,8 @@ const Gameboard = () => { } else if (pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) { alert('Yellow Won'); } - }, [pawns]); + }; + useEffect(() => { socket.emit('room:data', context.roomId); socket.on('room:data', data => { @@ -59,9 +60,8 @@ const Gameboard = () => { setTime(data.nextMoveTime); setStarted(data.started); }); - }, []); + }, [socket]); - // Callback to handle dice rolling between dice and map component const rolledNumberCallback = number => { setRolledNumber(number); }; diff --git a/src/components/Gameboard/Map/Map.jsx b/src/components/Gameboard/Map/Map.jsx new file mode 100644 index 0000000..1af250e --- /dev/null +++ b/src/components/Gameboard/Map/Map.jsx @@ -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); + } + }; + 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); + } + }; + }; + useEffect(() => { + rerenderCanvas(); + }, [hintPawn, pawns, rerenderCanvas]); + + return ( + + ); +}; +export default Map; diff --git a/src/components/Gameboard/Map/canPawnMove.js b/src/components/Gameboard/Map/canPawnMove.js new file mode 100644 index 0000000..1a5de63 --- /dev/null +++ b/src/components/Gameboard/Map/canPawnMove.js @@ -0,0 +1,26 @@ +export default (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; + } +}; diff --git a/backend/utils/functions.js b/src/components/Gameboard/Map/getPositionAfterMove.js similarity index 79% rename from backend/utils/functions.js rename to src/components/Gameboard/Map/getPositionAfterMove.js index 81f5522..6b403ca 100644 --- a/backend/utils/functions.js +++ b/src/components/Gameboard/Map/getPositionAfterMove.js @@ -1,19 +1,4 @@ -const { colors } = require('./constants'); -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) { +export default (pawn, rolledNumber) => { const { position, color } = pawn; switch (color) { case 'red': @@ -71,5 +56,4 @@ function getPawnPositionAfterMove(rolledNumber, pawn) { return position; } } -} -module.exports = { getStartPositions, getPawnPositionAfterMove }; +}; diff --git a/src/components/game-board-components/positions.js b/src/components/Gameboard/positions.js similarity index 100% rename from src/components/game-board-components/positions.js rename to src/components/Gameboard/positions.js diff --git a/src/components/LoginPage/AddServer/AddServer.jsx b/src/components/LoginPage/AddServer/AddServer.jsx index 054bf54..4333c50 100644 --- a/src/components/LoginPage/AddServer/AddServer.jsx +++ b/src/components/LoginPage/AddServer/AddServer.jsx @@ -10,7 +10,6 @@ const AddServer = () => { useEffect(() => { socket.on('room:created', () => { - console.log('ewa'); socket.emit('room:rooms'); }); }, []); diff --git a/src/components/LoginPage/NameInput/NameInput.jsx b/src/components/LoginPage/NameInput/NameInput.jsx index e1212e4..307518a 100644 --- a/src/components/LoginPage/NameInput/NameInput.jsx +++ b/src/components/LoginPage/NameInput/NameInput.jsx @@ -1,13 +1,14 @@ import React, { useState, useContext, useEffect } from 'react'; import { SocketContext } from '../../../App'; +import useInput from '../../../hooks/useInput'; import './NameInput.css'; const NameInput = ({ isRoomPrivate, roomId }) => { const socket = useContext(SocketContext); - const [nickname, setNickname] = useState(''); - const [password, setPassword] = useState(''); + const nickname = useInput(''); + const password = useInput(''); const [isPasswordWrong, setIsPasswordWrong] = useState(false); const handleButtonClick = () => { - socket.emit('player:login', { name: nickname, password: password, roomId: roomId }); + socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId }); }; useEffect(() => { socket.on('error:wrongPassword', () => { @@ -28,12 +29,12 @@ const NameInput = ({ isRoomPrivate, roomId }) => { return (
- setNickname(e.target.value)} /> + {isRoomPrivate ? ( setPassword(e.target.value)} + onChange={password.onChange} style={{ backgroundColor: isPasswordWrong ? 'red' : null }} /> ) : null} diff --git a/src/components/LoginPage/ServerList/ServerList.jsx b/src/components/LoginPage/ServerList/ServerList.jsx index 28e55a2..c23601e 100644 --- a/src/components/LoginPage/ServerList/ServerList.jsx +++ b/src/components/LoginPage/ServerList/ServerList.jsx @@ -12,7 +12,7 @@ const ServerList = () => { const [rooms, setRooms] = useState([]); const [joining, setJoining] = useState(false); const [clickedRoom, setClickedRoom] = useState(null); - useEffect(async () => { + useEffect(() => { socket.emit('room:rooms'); socket.on('room:rooms', data => { data = JSON.parse(data); @@ -70,9 +70,7 @@ const ServerList = () => {
)}
- {joining ? ( - - ) : null} + {joining ? : null}
); }; diff --git a/src/components/navbar-components/AnimatedOverlay.jsx b/src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx similarity index 67% rename from src/components/navbar-components/AnimatedOverlay.jsx rename to src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx index 5fc82fa..ba0558a 100644 --- a/src/components/navbar-components/AnimatedOverlay.jsx +++ b/src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx @@ -1,13 +1,9 @@ -import React, { useState, useEffect } from 'react'; +import React, { useMemo } from 'react'; import { CSSTransition } from 'react-transition-group'; import './TimerAnimation.js'; const AnimatedOverlay = ({ time }) => { - const [animationDelay, setAnimationDelay] = useState(); - - useEffect(() => { - setAnimationDelay(15 - Math.ceil((time - Date.now()) / 1000)); - }, [time]); + const animationDelay = useMemo(() => 15 - Math.ceil((time - Date.now()) / 1000), [time]); return ( { return ( diff --git a/src/components/Navbar.css b/src/components/Navbar/Navbar.css similarity index 100% rename from src/components/Navbar.css rename to src/components/Navbar/Navbar.css diff --git a/src/components/Navbar.jsx b/src/components/Navbar/Navbar.jsx similarity index 82% rename from src/components/Navbar.jsx rename to src/components/Navbar/Navbar.jsx index 0c67c8a..1b05d12 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar/Navbar.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import Dice from './game-board-components/Dice'; -import NameContainer from './navbar-components/NameContainer'; -import ReadyButton from './navbar-components/ReadyButton'; +import Dice from '../Gameboard/Dice/Dice'; +import NameContainer from './NameContainer/NameContainer'; +import ReadyButton from './ReadyButton/ReadyButton'; import './Navbar.css'; import { useContext } from 'react'; -import { PlayerDataContext } from '../App'; +import { PlayerDataContext } from '../../App'; const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => { const context = useContext(PlayerDataContext); const colors = ['red', 'blue', 'green', 'yellow']; diff --git a/src/components/navbar-components/ReadyButton.jsx b/src/components/Navbar/ReadyButton/ReadyButton.jsx similarity index 77% rename from src/components/navbar-components/ReadyButton.jsx rename to src/components/Navbar/ReadyButton/ReadyButton.jsx index 6addc94..6249a9e 100644 --- a/src/components/navbar-components/ReadyButton.jsx +++ b/src/components/Navbar/ReadyButton/ReadyButton.jsx @@ -1,19 +1,17 @@ import React, { useState, useContext, useEffect } from 'react'; -import { SocketContext } from '../../App'; +import { SocketContext } from '../../../App'; import Switch from '@material-ui/core/Switch'; import '../Navbar.css'; -import './TimerAnimation'; +import '../NameContainer/AnimatedOverlay/TimerAnimation'; const ReadyButton = ({ isReady }) => { const socket = useContext(SocketContext); - const [checked, setChecked] = useState(); + const [checked, setChecked] = useState(isReady); + const handleCheckboxChange = () => { socket.emit('player:ready'); setChecked(!checked); }; - useEffect(() => { - setChecked(isReady); - }); return (
diff --git a/src/components/game-board-components/Map.jsx b/src/components/game-board-components/Map.jsx deleted file mode 100644 index 0d93155..0000000 --- a/src/components/game-board-components/Map.jsx +++ /dev/null @@ -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 ( - - ); -}; -export default Map; diff --git a/src/constants/pawnImages.js b/src/constants/pawnImages.js new file mode 100644 index 0000000..e568106 --- /dev/null +++ b/src/constants/pawnImages.js @@ -0,0 +1,13 @@ +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'; + +export default { + green: greenPawn, + blue: bluePawn, + red: redPawn, + yellow: yellowPawn, + grey: greyPawn, +}; diff --git a/src/constants/positions.js b/src/constants/positions.js new file mode 100644 index 0000000..c437f9f --- /dev/null +++ b/src/constants/positions.js @@ -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; diff --git a/src/hooks/useInput.js b/src/hooks/useInput.js new file mode 100644 index 0000000..1144f96 --- /dev/null +++ b/src/hooks/useInput.js @@ -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, + }; +} diff --git a/src/images/map.jpg b/src/images/map.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c158fd510ae0d3b49323f56b092de8d342ef022 GIT binary patch literal 40140 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 I($Crd2U~y9zW@LL literal 0 HcmV?d00001