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..8a5bf2a --- /dev/null +++ b/backend/controllers/roomController.js @@ -0,0 +1,30 @@ +const Room = require('../models/room'); +const { sendToPlayersData } = require('../socket/emits'); + +const getRoom = async roomId => { + return await Room.findOne({ _id: roomId }).exec(); +}; + +const getRooms = async () => { + return await Room.find().exec(); +}; + +const updateRoom = async room => { + return await Room.findOneAndUpdate({ _id: room._id }, room).exec(); +}; + +const getJoinableRoom = async () => { + return await Room.findOne({ full: false, started: false }).exec(); +}; + +const createNewRoom = data => { + const room = new Room(data); + room.save(); + return room; +}; + +Room.watch().on('change', async data => { + sendToPlayersData(await getRoom(data.documentKey._id)); +}); + +module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom }; diff --git a/backend/handlers/gameHandler.js b/backend/handlers/gameHandler.js index b4f244c..9ef9315 100644 --- a/backend/handlers/gameHandler.js +++ b/backend/handlers/gameHandler.js @@ -1,88 +1,33 @@ -const Room = require('../schemas/room'); -const { getPawnPositionAfterMove } = require('../utils/functions'); +const { getRoom, updateRoom } = require('../controllers/roomController'); +const { sendToPlayersRolledNumber } = require('../socket/emits'); +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)) { - const newPositionOfMovedPawn = getPawnPositionAfterMove(room.rolledNumber, pawn); + if (isMoveValid(req.session, pawn, room)) { + const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber); 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..825d568 100644 --- a/backend/handlers/playerHandler.js +++ b/backend/handlers/playerHandler.js @@ -1,34 +1,24 @@ -const RoomModel = require('../schemas/room'); +const { getRoom, updateRoom } = 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 (room) { - addPlayerToExistingRoom(room, data); - } else { - createNewRoom(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 () => { - 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)); - }; - - const createNewRoom = async data => { - const room = new RoomModel(); - room.addPlayer(data.name); - await room.save(); - reloadSession(room); + await updateRoom(room); }; const addPlayerToExistingRoom = async (room, data) => { @@ -36,7 +26,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 bfdeef3..f99324c 100644 --- a/backend/handlers/roomHandler.js +++ b/backend/handlers/roomHandler.js @@ -1,19 +1,31 @@ -const RoomModel = require('../schemas/room'); +const { getRooms, getRoom, updateRoom, createNewRoom } = require('../controllers/roomController'); +const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits'); -module.exports = (io, socket) => { +module.exports = socket => { const req = socket.request; - const getData = async () => { - let room = await RoomModel.findOne({ _id: req.session.roomId }); + 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. 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); }; - socket.on('room:data', getData); + + const handleGetAllRooms = async () => { + let rooms = await getRooms(); + sendToOnePlayerRooms(socket.id, rooms); + }; + + 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/pawn.js b/backend/models/pawn.js new file mode 100644 index 0000000..2680cad --- /dev/null +++ b/backend/models/pawn.js @@ -0,0 +1,82 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const PawnSchema = new Schema({ + color: String, + basePos: Number, + position: Number, +}); + +PawnSchema.methods.canMove = function (rolledNumber) { + if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) { + return true; + } + // (if player's pawn is near finish line) if the move does not go beyond the win line + if (this.position !== this.getPositionAfterMove(rolledNumber) && this.position !== this.basePos) { + return true; + } + return false; +}; + +PawnSchema.methods.getPositionAfterMove = function (rolledNumber) { + const { position, color } = this; + switch (color) { + case 'red': + if (position + rolledNumber <= 73) { + if (position >= 0 && position <= 3) { + return 16; + } else if (position <= 66 && position + rolledNumber >= 67) { + return position + rolledNumber + 1; + } else { + return position + rolledNumber; + } + } else { + return position; + } + case 'blue': + if (position + rolledNumber <= 79) { + if (position >= 4 && position <= 7) { + return 55; + } else if (position <= 67 && position + rolledNumber > 67) { + return position + rolledNumber - 52; + } else if (position <= 53 && position + rolledNumber >= 54) { + return position + rolledNumber + 20; + } else { + return position + rolledNumber; + } + } else { + return position; + } + case 'green': + if (position + rolledNumber <= 85) { + if (position >= 8 && position <= 11) { + return 42; + } else if (position <= 67 && position + rolledNumber > 67) { + return position + rolledNumber - 52; + } else if (position <= 40 && position + rolledNumber >= 41) { + return position + rolledNumber + 39; + } else { + return position + rolledNumber; + } + } else { + return position; + } + case 'yellow': + if (position + rolledNumber <= 85) { + if (position >= 12 && position <= 15) { + return 29; + } else if (position <= 67 && position + rolledNumber > 67) { + return position + rolledNumber - 52; + } else if (position <= 27 && position + rolledNumber >= 28) { + return position + rolledNumber + 58; + } else { + return position + rolledNumber; + } + } else { + return position; + } + } +}; + +module.exports = PawnSchema; 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 73% rename from backend/schemas/room.js rename to backend/models/room.js index e171950..8c3f25a 100644 --- a/backend/schemas/room.js +++ b/backend/models/room.js @@ -1,11 +1,13 @@ 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: false }, + password: String, createDate: { type: Date, default: Date.now }, started: { type: Boolean, default: false }, full: { type: Boolean, default: false }, @@ -13,7 +15,23 @@ const RoomSchema = new 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) { @@ -37,11 +55,11 @@ 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) { - const newPositionOfMovedPawn = getPawnPositionAfterMove(this.rolledNumber, pawn); + const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber); this.changePositionOfPawn(pawn, newPositionOfMovedPawn); this.beatPawns(newPositionOfMovedPawn, pawn.color); }; @@ -66,6 +84,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._id.toString()); }; RoomSchema.methods.isFull = function () { @@ -79,9 +98,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], @@ -104,6 +124,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/schemas/pawn.js b/backend/schemas/pawn.js deleted file mode 100644 index 3df2429..0000000 --- a/backend/schemas/pawn.js +++ /dev/null @@ -1,24 +0,0 @@ -const mongoose = require('mongoose'); - -const Schema = mongoose.Schema; - -const { getPawnPositionAfterMove } = require('../utils/functions'); - -const PawnSchema = new Schema({ - color: String, - basePos: Number, - position: Number, -}); - -PawnSchema.methods.canMove = function (rolledNumber) { - if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) { - return true; - } - // (if player's pawn is near finish line) if the move does not go beyond the win line - if (this.position !== getPawnPositionAfterMove(rolledNumber, this) && this.position !== this.basePos) { - return true; - } - return false; -}; - -module.exports = PawnSchema; diff --git a/backend/server.js b/backend/server.js index fb85acb..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')); @@ -91,4 +36,4 @@ if (process.env.NODE_ENV === 'production') { }); } -module.exports = { server }; \ No newline at end of file +module.exports = { server }; 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; diff --git a/src/App.js b/src/App.js index fa208b4..6853ef8 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 Gameboard from './components/Gameboard'; -import NameInput from './components/NameInput'; +import ReactLoading from 'react-loading'; +import Gameboard from './components/Gameboard/Gameboard'; +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/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.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..4333c50 --- /dev/null +++ b/src/components/LoginPage/AddServer/AddServer.jsx @@ -0,0 +1,57 @@ +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', () => { + 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 new file mode 100644 index 0000000..3376c6f --- /dev/null +++ b/src/components/LoginPage/LoginPage.css @@ -0,0 +1,53 @@ +.login-page-container { + display: flex; + flex-direction: row; + + justify-content: center; + align-items: flex-start; + height: 50%; + width: 100%; +} + +.lp-container { + margin: 50px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 500px; + padding: 20px; + color: white; +} + +.title-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + height: 40px; + border: 1px solid white; + border-radius: 2px; + transform: scaleX(1.02); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + padding-left: 10px; + text-align: center; +} + +.title-container > h1 { + width: 100%; + margin: 0; + padding: 0; +} + +.content-container { + display: flex; + flex-direction: column; + width: 100%; + padding: 10px; + background-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + border-left: 1px solid black; + border-right: 1px solid black; + border-bottom: 1px solid black; +} diff --git a/src/components/LoginPage/LoginPage.jsx b/src/components/LoginPage/LoginPage.jsx new file mode 100644 index 0000000..9532e2d --- /dev/null +++ b/src/components/LoginPage/LoginPage.jsx @@ -0,0 +1,16 @@ +import './LoginPage.css'; +import AddServer from './AddServer/AddServer'; +import ServerList from './ServerList/ServerList'; +import NameInput from './NameInput/NameInput'; +const LoginPage = () => { + return ( + <> +
+ + +
+ + ); +}; + +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..9f6282c --- /dev/null +++ b/src/components/LoginPage/NameInput/NameInput.css @@ -0,0 +1,70 @@ +.name-input-container { + display: flex; + flex-direction: column; + padding: 10px 20px 60px 20px; + width: 300px; + background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%); + border: 1px solid white; + border-radius: 8px; + margin: 20px; +} +.name-input-container > button { + margin-top: 5px; + text-align: center; + width: 100px; + align-self: center; +} +.name-input-container > input { + margin-top: 10px; +} +.name-overlay { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1; +} +input, +button { + padding: 0; + border: none; + outline: none; + box-sizing: border-box; +} + +input { + width: 100%; + padding: 12px; + font-size: 16px; + border-radius: 8px; + color: white; + border: 1px solid #ccc; + background-color: rgba(0, 0, 0, 0.2); + transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out; +} + +input:focus { + color: black; + border-color: #4a90e2; + background-color: #fff; +} + +button { + padding: 12px 20px; + font-size: 16px; + border-radius: 8px; + border: none; + color: #fff; + background-color: rgba(0, 0, 0, 0.4); + cursor: pointer; + transition: background-color 0.3s ease-in-out; +} + +button:hover { + background-color: rgba(0, 0, 0, 1); +} diff --git a/src/components/LoginPage/NameInput/NameInput.jsx b/src/components/LoginPage/NameInput/NameInput.jsx new file mode 100644 index 0000000..307518a --- /dev/null +++ b/src/components/LoginPage/NameInput/NameInput.jsx @@ -0,0 +1,47 @@ +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 = useInput(''); + const password = useInput(''); + const [isPasswordWrong, setIsPasswordWrong] = useState(false); + const handleButtonClick = () => { + socket.emit('player:login', { name: nickname.value, password: password.value, 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 ( +
+
+ + {isRoomPrivate ? ( + + ) : null} + +
+
+ ); +}; + +export default NameInput; 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..c23601e --- /dev/null +++ b/src/components/LoginPage/ServerList/ServerList.jsx @@ -0,0 +1,77 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { SocketContext } from '../../../App'; +import lock from '../../../images/login-page/lock.png'; +import refresh from '../../../images/login-page/refresh.png'; +import ReactLoading from 'react-loading'; + +import './ServerList.css'; +import NameInput from '../NameInput/NameInput'; + +const ServerList = () => { + const socket = useContext(SocketContext); + const [rooms, setRooms] = useState([]); + const [joining, setJoining] = useState(false); + const [clickedRoom, setClickedRoom] = useState(null); + useEffect(() => { + socket.emit('room:rooms'); + socket.on('room:rooms', data => { + data = JSON.parse(data); + setRooms(data); + }); + }, []); + + 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/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/components/navbar-components/AnimatedOverlay.jsx b/src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx similarity index 57% rename from src/components/navbar-components/AnimatedOverlay.jsx rename to src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx index 82b1507..ba0558a 100644 --- a/src/components/navbar-components/AnimatedOverlay.jsx +++ b/src/components/Navbar/NameContainer/AnimatedOverlay/AnimatedOverlay.jsx @@ -1,20 +1,16 @@ -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 (
diff --git a/src/components/navbar-components/TimerAnimation.js b/src/components/Navbar/NameContainer/AnimatedOverlay/TimerAnimation.js similarity index 100% rename from src/components/navbar-components/TimerAnimation.js rename to src/components/Navbar/NameContainer/AnimatedOverlay/TimerAnimation.js diff --git a/src/components/navbar-components/NameContainer.jsx b/src/components/Navbar/NameContainer/NameContainer.jsx similarity index 89% rename from src/components/navbar-components/NameContainer.jsx rename to src/components/Navbar/NameContainer/NameContainer.jsx index b0a68cc..14981ee 100644 --- a/src/components/navbar-components/NameContainer.jsx +++ b/src/components/Navbar/NameContainer/NameContainer.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import AnimatedOverlay from './AnimatedOverlay'; +import AnimatedOverlay from './AnimatedOverlay/AnimatedOverlay'; const NameContainer = ({ player, time }) => { 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/login-page/lock.png b/src/images/login-page/lock.png new file mode 100644 index 0000000..8e56993 Binary files /dev/null and b/src/images/login-page/lock.png differ diff --git a/src/images/login-page/refresh.png b/src/images/login-page/refresh.png new file mode 100644 index 0000000..25988e6 Binary files /dev/null and b/src/images/login-page/refresh.png differ diff --git a/src/images/login-page/user.png b/src/images/login-page/user.png new file mode 100644 index 0000000..0fa4040 Binary files /dev/null and b/src/images/login-page/user.png differ diff --git a/src/images/map.jpg b/src/images/map.jpg new file mode 100644 index 0000000..8c158fd Binary files /dev/null and b/src/images/map.jpg differ