Merge pull request #4 from Wenszel/feature-selecting-room

added login page
This commit is contained in:
Wiktor Smaga 2023-12-08 19:49:29 +01:00 committed by GitHub
commit 9d2900ec6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1069 additions and 512 deletions

View File

@ -0,0 +1,14 @@
const CONNECTION_URI = require('../credentials.js');
module.exports = function (mongoose) {
mongoose.set('useFindAndModify', false);
mongoose
.connect(CONNECTION_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log('MongoDB Connected…');
})
.catch(err => console.error(err));
};

26
backend/config/socket.js Normal file
View File

@ -0,0 +1,26 @@
const socketManager = require('../socket/socketManager');
const registerPlayerHandlers = require('../handlers/playerHandler');
const registerRoomHandlers = require('../handlers/roomHandler');
const registerGameHandlers = require('../handlers/gameHandler');
const { sessionMiddleware, wrap } = require('../config/session');
module.exports = function (server) {
socketManager.initialize(server);
socketManager.getIO().engine.on('initial_headers', (headers, req) => {
if (req.cookieHolder) {
headers['set-cookie'] = req.cookieHolder;
delete req.cookieHolder;
}
});
socketManager.getIO().use(wrap(sessionMiddleware));
socketManager.getIO().on('connection', socket => {
registerPlayerHandlers(socket);
registerRoomHandlers(socket);
registerGameHandlers(socket);
if (socket.request.session.roomId) {
const roomId = socket.request.session.roomId.toString();
socket.join(roomId);
socket.emit('player:data', JSON.stringify(socket.request.session));
}
});
};

View File

@ -0,0 +1,30 @@
const Room = require('../models/room');
const { sendToPlayersData } = require('../socket/emits');
const getRoom = async roomId => {
return await Room.findOne({ _id: roomId }).exec();
};
const getRooms = async () => {
return await Room.find().exec();
};
const updateRoom = async room => {
return await Room.findOneAndUpdate({ _id: room._id }, room).exec();
};
const getJoinableRoom = async () => {
return await Room.findOne({ full: false, started: false }).exec();
};
const createNewRoom = data => {
const room = new Room(data);
room.save();
return room;
};
Room.watch().on('change', async data => {
sendToPlayersData(await getRoom(data.documentKey._id));
});
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom };

View File

@ -1,86 +1,31 @@
const Room = require('../schemas/room'); const { getRoom, updateRoom } = require('../controllers/roomController');
const { getPawnPositionAfterMove } = require('../utils/functions'); const { sendToPlayersRolledNumber } = require('../socket/emits');
const { rollDice, isMoveValid } = require('./handlersFunctions');
module.exports = (io, socket) => { module.exports = socket => {
const req = socket.request; const req = socket.request;
const handleMovePawn = async pawnId => { const handleMovePawn = async pawnId => {
const room = await getRoom(); const room = await getRoom(req.session.roomId);
const pawn = room.getPawn(pawnId); const pawn = room.getPawn(pawnId);
if (isMoveValid(pawn, room)) { if (isMoveValid(req.session, pawn, room)) {
const newPositionOfMovedPawn = getPawnPositionAfterMove(room.rolledNumber, pawn); const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
room.changePositionOfPawn(pawn, newPositionOfMovedPawn); room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
room.beatPawns(newPositionOfMovedPawn, req.session.color); room.beatPawns(newPositionOfMovedPawn, req.session.color);
handleChangeOfPlayer(room); room.changeMovingPlayer();
await updateRoom(room);
} }
}; };
const handleRollDice = async () => { const handleRollDice = async () => {
const rolledNumber = rollDice(); const rolledNumber = rollDice();
const room = await updateRoom({ rolledNumber: rolledNumber }); sendToPlayersRolledNumber(req.session.roomId, rolledNumber);
if (!canPlayerMove(room, rolledNumber)) { const room = await updateRoom({ _id: req.session.roomId, rolledNumber: rolledNumber });
handleChangeOfPlayer(room); const player = room.getPlayer(req.session.playerId);
} if (!player.canMove(room, rolledNumber)) {
};
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.changeMovingPlayer();
room.timeoutID = setTimeout(makeRandomMove, 15000, room);
await updateRoom(room); await updateRoom(room);
};
const makeRandomMove = async room => {
if (room.rolledNumber === null) room.rolledNumber = rollDice();
const pawnsThatCanMove = room.getPawnsThatCanMove()
if (pawnsThatCanMove.length > 0) {
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
room.movePawn(randomPawn);
} }
await handleChangeOfPlayer(room);
};
Room.watch().on('change', async () => {
sendToPlayersData(await getRoom());
});
const getRoom = async () => {
return await Room.findOne({ _id: req.session.roomId }).exec();
};
const updateRoom = async room => {
return await Room.findOneAndUpdate({ _id: req.session.roomId }, room).exec();
};
const sendToPlayersRolledNumber = rolledNumber => {
io.to(req.session.roomId).emit('game:roll', rolledNumber);
};
const sendToPlayersData = room => {
io.to(req.session.roomId).emit('room:data', JSON.stringify(room));
}; };
socket.on('game:roll', handleRollDice); socket.on('game:roll', handleRollDice);

View File

@ -0,0 +1,35 @@
const { sendToPlayersRolledNumber } = require('../socket/emits');
const rollDice = () => {
const rolledNumber = Math.ceil(Math.random() * 6);
return rolledNumber;
};
const makeRandomMove = async roomId => {
const { updateRoom, getRoom } = require('../controllers/roomController');
const room = await getRoom(roomId);
if (room.rolledNumber === null) {
room.rolledNumber = rollDice();
sendToPlayersRolledNumber(room._id.toString(), room.rolledNumber);
}
const pawnsThatCanMove = room.getPawnsThatCanMove();
if (pawnsThatCanMove.length > 0) {
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
room.movePawn(randomPawn);
}
room.changeMovingPlayer();
await updateRoom(room);
};
const isMoveValid = (session, pawn, room) => {
if (session.color !== pawn.color) {
return false;
}
if (session.playerId !== room.getCurrentlyMovingPlayer()._id.toString()) {
return false;
}
return true;
};
module.exports = { rollDice, makeRandomMove, isMoveValid };

View File

@ -1,34 +1,24 @@
const RoomModel = require('../schemas/room'); const { getRoom, updateRoom } = require('../controllers/roomController');
const { colors } = require('../utils/constants'); const { colors } = require('../utils/constants');
module.exports = (io, socket) => { module.exports = socket => {
const req = socket.request; const req = socket.request;
const handleLogin = async data => { const handleLogin = async data => {
const room = await RoomModel.findOne({ full: false, started: false }); const room = await getRoom(data.roomId);
if (room) { 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); addPlayerToExistingRoom(room, data);
} else {
createNewRoom(data);
}
}; };
const handleReady = async () => { const handleReady = async () => {
const { roomId, playerId } = req.session; const room = await getRoom(req.session.roomId);
const room = await RoomModel.findOne({ _id: roomId }); room.getPlayer(req.session.playerId).changeReadyStatus();
room.getPlayer(playerId).changeReadyStatus();
if (room.canStartGame()) { if (room.canStartGame()) {
room.startGame(); room.startGame();
} }
await RoomModel.findOneAndUpdate({ _id: roomId }, room); await updateRoom(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);
}; };
const addPlayerToExistingRoom = async (room, data) => { const addPlayerToExistingRoom = async (room, data) => {
@ -36,7 +26,7 @@ module.exports = (io, socket) => {
if (room.isFull()) { if (room.isFull()) {
room.startGame(); room.startGame();
} }
await RoomModel.findOneAndUpdate({ _id: room._id }, room); await updateRoom(room);
reloadSession(room); reloadSession(room);
}; };

View File

@ -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 req = socket.request;
const getData = async () => { const handleGetData = 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 // 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. // Typically, the responsibility for changing players is managed by gameHandler.js.
if (room.nextMoveTime <= Date.now()) { if (room.nextMoveTime <= Date.now()) {
room.changeMovingPlayer(); room.changeMovingPlayer();
await RoomModel.findOneAndUpdate({ _id: req.session.roomId }, room); await updateRoom(room);
io.to(req.session.roomId).emit('room:data', JSON.stringify(room));
} else {
io.to(socket.id).emit('room:data', JSON.stringify(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);
}; };

82
backend/models/pawn.js Normal file
View File

@ -0,0 +1,82 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PawnSchema = new Schema({
color: String,
basePos: Number,
position: Number,
});
PawnSchema.methods.canMove = function (rolledNumber) {
if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) {
return true;
}
// (if player's pawn is near finish line) if the move does not go beyond the win line
if (this.position !== this.getPositionAfterMove(rolledNumber) && this.position !== this.basePos) {
return true;
}
return false;
};
PawnSchema.methods.getPositionAfterMove = function (rolledNumber) {
const { position, color } = this;
switch (color) {
case 'red':
if (position + rolledNumber <= 73) {
if (position >= 0 && position <= 3) {
return 16;
} else if (position <= 66 && position + rolledNumber >= 67) {
return position + rolledNumber + 1;
} else {
return position + rolledNumber;
}
} else {
return position;
}
case 'blue':
if (position + rolledNumber <= 79) {
if (position >= 4 && position <= 7) {
return 55;
} else if (position <= 67 && position + rolledNumber > 67) {
return position + rolledNumber - 52;
} else if (position <= 53 && position + rolledNumber >= 54) {
return position + rolledNumber + 20;
} else {
return position + rolledNumber;
}
} else {
return position;
}
case 'green':
if (position + rolledNumber <= 85) {
if (position >= 8 && position <= 11) {
return 42;
} else if (position <= 67 && position + rolledNumber > 67) {
return position + rolledNumber - 52;
} else if (position <= 40 && position + rolledNumber >= 41) {
return position + rolledNumber + 39;
} else {
return position + rolledNumber;
}
} else {
return position;
}
case 'yellow':
if (position + rolledNumber <= 85) {
if (position >= 12 && position <= 15) {
return 29;
} else if (position <= 67 && position + rolledNumber > 67) {
return position + rolledNumber - 52;
} else if (position <= 27 && position + rolledNumber >= 28) {
return position + rolledNumber + 58;
} else {
return position + rolledNumber;
}
} else {
return position;
}
}
};
module.exports = PawnSchema;

View File

@ -3,6 +3,7 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
const PlayerSchema = new Schema({ const PlayerSchema = new Schema({
sessionID: String,
name: String, name: String,
color: String, color: String,
ready: { type: Boolean, default: false }, ready: { type: Boolean, default: false },
@ -13,4 +14,12 @@ PlayerSchema.methods.changeReadyStatus = function () {
this.ready = !this.ready; 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; module.exports = PlayerSchema;

View File

@ -1,11 +1,13 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { colors } = require('../utils/constants'); const { colors } = require('../utils/constants');
const { getPawnPositionAfterMove, getStartPositions } = require('../utils/functions'); const { makeRandomMove } = require('../handlers/handlersFunctions');
const Schema = mongoose.Schema;
const PawnSchema = require('./pawn'); const PawnSchema = require('./pawn');
const PlayerSchema = require('./player'); const PlayerSchema = require('./player');
const RoomSchema = new Schema({ const RoomSchema = new mongoose.Schema({
name: String,
private: { type: Boolean, default: false },
password: String,
createDate: { type: Date, default: Date.now }, createDate: { type: Date, default: Date.now },
started: { type: Boolean, default: false }, started: { type: Boolean, default: false },
full: { type: Boolean, default: false }, full: { type: Boolean, default: false },
@ -13,7 +15,23 @@ const RoomSchema = new Schema({
timeoutID: Number, timeoutID: Number,
rolledNumber: Number, rolledNumber: Number,
players: [PlayerSchema], 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) { RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
@ -37,11 +55,11 @@ RoomSchema.methods.changeMovingPlayer = function () {
this.nextMoveTime = Date.now() + 15000; this.nextMoveTime = Date.now() + 15000;
this.rolledNumber = null; this.rolledNumber = null;
if (this.timeoutID) clearTimeout(this.timeoutID); if (this.timeoutID) clearTimeout(this.timeoutID);
this.timeoutID = null; this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
}; };
RoomSchema.methods.movePawn = function (pawn) { RoomSchema.methods.movePawn = function (pawn) {
const newPositionOfMovedPawn = getPawnPositionAfterMove(this.rolledNumber, pawn); const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber);
this.changePositionOfPawn(pawn, newPositionOfMovedPawn); this.changePositionOfPawn(pawn, newPositionOfMovedPawn);
this.beatPawns(newPositionOfMovedPawn, pawn.color); this.beatPawns(newPositionOfMovedPawn, pawn.color);
}; };
@ -66,6 +84,7 @@ RoomSchema.methods.startGame = function () {
this.nextMoveTime = Date.now() + 15000; this.nextMoveTime = Date.now() + 15000;
this.players.forEach(player => (player.ready = true)); this.players.forEach(player => (player.ready = true));
this.players[0].nowMoving = true; this.players[0].nowMoving = true;
this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString());
}; };
RoomSchema.methods.isFull = function () { RoomSchema.methods.isFull = function () {
@ -79,9 +98,10 @@ RoomSchema.methods.getPlayer = function (playerId) {
return this.players.find(player => player._id.toString() === playerId.toString()); 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; if (this.full) return;
this.players.push({ this.players.push({
sessionID: id,
name: name, name: name,
ready: false, ready: false,
color: colors[this.players.length], color: colors[this.players.length],
@ -104,6 +124,6 @@ RoomSchema.methods.getCurrentlyMovingPlayer = function () {
return this.players.find(player => player.nowMoving === true); return this.players.find(player => player.nowMoving === true);
}; };
const RoomModel = mongoose.model('Room', RoomSchema); const Room = mongoose.model('Room', RoomSchema);
module.exports = RoomModel; module.exports = Room;

View File

@ -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;

View File

@ -1,13 +1,11 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const { sessionMiddleware, wrap } = require('./controllers/serverController');
const registerPlayerHandlers = require('./handlers/playerHandler');
const registerRoomHandlers = require('./handlers/roomHandler');
const registerGameHandlers = require('./handlers/gameHandler');
const PORT = 8080;
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const CONNECTION_URI = require('./credentials.js'); const { sessionMiddleware } = require('./config/session');
const PORT = 8080;
const app = express(); const app = express();
app.use(cookieParser()); app.use(cookieParser());
@ -26,63 +24,10 @@ app.use(
); );
app.use(sessionMiddleware); app.use(sessionMiddleware);
mongoose.set('useFindAndModify', false); const server = app.listen(PORT);
mongoose
.connect(CONNECTION_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log('MongoDB Connected…');
})
.catch(err => console.error(err));
const server = app.listen(PORT, () => { require('./config/database')(mongoose);
console.log('Server runs on port ' + PORT); require('./config/socket')(server);
});
const io = require('socket.io')(server, {
cors: {
origin: 'http://localhost:3000',
credentials: true,
},
allowRequest: (req, callback) => {
const fakeRes = {
getHeader() {
return [];
},
setHeader(key, values) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req, fakeRes, () => {
if (req.session) {
fakeRes.writeHead();
req.session.save();
}
callback(null, true);
});
},
});
io.engine.on('initial_headers', (headers, req) => {
if (req.cookieHolder) {
headers['set-cookie'] = req.cookieHolder;
delete req.cookieHolder;
}
});
io.use(wrap(sessionMiddleware));
io.on('connection', socket => {
registerPlayerHandlers(io, socket);
registerRoomHandlers(io, socket);
registerGameHandlers(io, socket);
if (socket.request.session.roomId) {
const roomId = socket.request.session.roomId.toString();
socket.join(roomId);
socket.emit('player:data', JSON.stringify(socket.request.session));
}
});
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
app.use(express.static('/app/build')); app.use(express.static('/app/build'));

19
backend/socket/emits.js Normal file
View File

@ -0,0 +1,19 @@
const socketManager = require('./socketManager');
const sendToPlayersRolledNumber = (id, rolledNumber) => {
socketManager.getIO().to(id).emit('game:roll', rolledNumber);
};
const sendToPlayersData = room => {
socketManager.getIO().to(room._id.toString()).emit('room:data', JSON.stringify(room));
};
const sendToOnePlayerData = (id, room) => {
socketManager.getIO().to(id).emit('room:data', JSON.stringify(room));
};
const sendToOnePlayerRooms = (id, rooms) => {
socketManager.getIO().to(id).emit('room:rooms', JSON.stringify(rooms));
};
module.exports = { sendToPlayersData, sendToPlayersRolledNumber, sendToOnePlayerData, sendToOnePlayerRooms };

View File

@ -0,0 +1,39 @@
const { sessionMiddleware } = require('../config/session');
const socketManager = {
io: null,
initialize(server) {
this.io = require('socket.io')(server, {
cors: {
origin: 'http://localhost:3000',
credentials: true,
},
allowRequest: (req, callback) => {
const fakeRes = {
getHeader() {
return [];
},
setHeader(key, values) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req, fakeRes, () => {
if (req.session) {
fakeRes.writeHead();
req.session.save();
}
callback(null, true);
});
},
});
},
getIO() {
if (!this.io) {
throw new Error('Socket.io not initialized');
}
return this.io;
},
};
module.exports = socketManager;

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState, createContext } from 'react'; import React, { useEffect, useState, createContext } from 'react';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
import ReactLoading from 'react-loading';
import Gameboard from './components/Gameboard'; import Gameboard from './components/Gameboard/Gameboard';
import NameInput from './components/NameInput'; import LoginPage from './components/LoginPage/LoginPage';
export const PlayerDataContext = createContext(); export const PlayerDataContext = createContext();
export const SocketContext = createContext(); export const SocketContext = createContext();
@ -32,7 +32,11 @@ function App() {
LOADING... LOADING...
</Route> </Route>
<Route path='/login'> <Route path='/login'>
<NameInput /> {playerSocket ? (
<LoginPage />
) : (
<ReactLoading type='spinningBubbles' color='white' height={667} width={375} />
)}
</Route> </Route>
<Route path='/game'> <Route path='/game'>
{playerData ? ( {playerData ? (

View File

@ -1,24 +1,28 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useEffect, useContext } from 'react';
import { SocketContext } from '../../App'; import { SocketContext } from '../../../App';
import one from '../../images/dice/1.png'; import one from '../../../images/dice/1.png';
import two from '../../images/dice/2.png'; import two from '../../../images/dice/2.png';
import three from '../../images/dice/3.png'; import three from '../../../images/dice/3.png';
import four from '../../images/dice/4.png'; import four from '../../../images/dice/4.png';
import five from '../../images/dice/5.png'; import five from '../../../images/dice/5.png';
import six from '../../images/dice/6.png'; import six from '../../../images/dice/6.png';
import roll from '../../images/dice/roll.png'; import roll from '../../../images/dice/roll.png';
const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => { const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
const [images] = useState([one, two, three, four, five, six, roll]);
const images = [one, two, three, four, five, six, roll];
const handleRoll = () => { const handleRoll = () => {
socket.emit('game:roll'); socket.emit('game:roll');
}; };
useEffect(() => { useEffect(() => {
socket.on('game:roll', number => { socket.on('game:roll', number => {
rolledNumberCallback(number); rolledNumberCallback(number);
}); });
}, []); }, []);
return ( return (
<div className={`dice-container dice-${color}`}> <div className={`dice-container dice-${color}`}>
{movingPlayer === color ? ( {movingPlayer === color ? (

View File

@ -1,17 +1,16 @@
import React, { useState, useEffect, useContext, useCallback } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import ReactLoading from 'react-loading'; import ReactLoading from 'react-loading';
import { PlayerDataContext, SocketContext } from '../App'; import { PlayerDataContext, SocketContext } from '../../App';
import Map from './game-board-components/Map'; import Map from './Map/Map';
import Navbar from './Navbar'; import Navbar from '../Navbar/Navbar';
const Gameboard = () => { const Gameboard = () => {
// Context data
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
const context = useContext(PlayerDataContext); const context = useContext(PlayerDataContext);
// Render data
const [pawns, setPawns] = useState([]); const [pawns, setPawns] = useState([]);
const [players, setPlayers] = useState([]); const [players, setPlayers] = useState([]);
// Game logic data
const [rolledNumber, setRolledNumber] = useState(null); const [rolledNumber, setRolledNumber] = useState(null);
const [time, setTime] = useState(); const [time, setTime] = useState();
const [isReady, setIsReady] = useState(); const [isReady, setIsReady] = useState();
@ -19,7 +18,8 @@ const Gameboard = () => {
const [started, setStarted] = useState(false); const [started, setStarted] = useState(false);
const [movingPlayer, setMovingPlayer] = useState('red'); const [movingPlayer, setMovingPlayer] = useState('red');
const checkWin = useCallback(() => {
const checkWin = () => {
// Player wins when all pawns with same color are inside end base // Player wins when all pawns with same color are inside end base
if (pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) { if (pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) {
alert('Red Won'); alert('Red Won');
@ -30,7 +30,8 @@ const Gameboard = () => {
} else if (pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) { } else if (pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) {
alert('Yellow Won'); alert('Yellow Won');
} }
}, [pawns]); };
useEffect(() => { useEffect(() => {
socket.emit('room:data', context.roomId); socket.emit('room:data', context.roomId);
socket.on('room:data', data => { socket.on('room:data', data => {
@ -59,9 +60,8 @@ const Gameboard = () => {
setTime(data.nextMoveTime); setTime(data.nextMoveTime);
setStarted(data.started); setStarted(data.started);
}); });
}, []); }, [socket]);
// Callback to handle dice rolling between dice and map component
const rolledNumberCallback = number => { const rolledNumberCallback = number => {
setRolledNumber(number); setRolledNumber(number);
}; };

View File

@ -0,0 +1,111 @@
import React, { useEffect, useRef, useState, useContext } from 'react';
import { PlayerDataContext, SocketContext } from '../../../App';
import mapImage from '../../../images/map.jpg';
import positions from '../positions';
import pawnImages from '../../../constants/pawnImages';
import canPawnMove from './canPawnMove';
import getPositionAfterMove from './getPositionAfterMove';
const Map = ({ pawns, nowMoving, rolledNumber }) => {
const player = useContext(PlayerDataContext);
const socket = useContext(SocketContext);
const canvasRef = useRef(null);
const [hintPawn, setHintPawn] = useState();
const paintPawn = (context, x, y, color) => {
const touchableArea = new Path2D();
touchableArea.arc(x, y, 12, 0, 2 * Math.PI);
const image = new Image();
image.src = pawnImages[color];
image.onload = function () {
context.drawImage(image, x - 17, y - 14, 35, 30);
};
return touchableArea;
};
const handleCanvasClick = event => {
if (hintPawn) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect(),
cursorX = event.clientX - rect.left,
cursorY = event.clientY - rect.top;
for (const pawn of pawns) {
if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
socket.emit('game:move', pawn._id);
}
}
setHintPawn(null);
}
};
const handleMouseMove = event => {
if (nowMoving && rolledNumber) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect(),
x = event.clientX - rect.left,
y = event.clientY - rect.top;
canvas.style.cursor = 'default';
for (const pawn of pawns) {
if (pawn.touchableArea) {
if (
ctx.isPointInPath(pawn.touchableArea, x, y) &&
player.color === pawn.color &&
canPawnMove(pawn, rolledNumber)
) {
const pawnPosition = getPositionAfterMove(pawn, rolledNumber);
if (pawnPosition) {
canvas.style.cursor = 'pointer';
setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' });
break;
}
} else {
setHintPawn(null);
}
} else {
setHintPawn(null);
}
}
} else {
setHintPawn(null);
}
};
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 (
<canvas
className='canvas-container'
width={460}
height={460}
ref={canvasRef}
onClick={handleCanvasClick}
onMouseMove={handleMouseMove}
/>
);
};
export default Map;

View File

@ -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;
}
};

View File

@ -1,19 +1,4 @@
const { colors } = require('./constants'); export default (pawn, rolledNumber) => {
function getStartPositions() {
const startPositions = [];
for (let i = 0; i < 16; i++) {
let pawn = {};
pawn.basePos = i;
pawn.position = i;
if (i < 4) pawn.color = colors[0];
else if (i < 8) pawn.color = colors[1];
else if (i < 12) pawn.color = colors[2];
else if (i < 16) pawn.color = colors[3];
startPositions.push(pawn);
}
return startPositions;
}
function getPawnPositionAfterMove(rolledNumber, pawn) {
const { position, color } = pawn; const { position, color } = pawn;
switch (color) { switch (color) {
case 'red': case 'red':
@ -71,5 +56,4 @@ function getPawnPositionAfterMove(rolledNumber, pawn) {
return position; return position;
} }
} }
} };
module.exports = { getStartPositions, getPawnPositionAfterMove };

View File

@ -0,0 +1,35 @@
.refresh {
display: flex;
margin-left: auto;
justify-content: center;
align-items: center;
width: 40px;
height: 100%;
border: 1px solid white;
}
.refresh > img {
width: 20px;
height: 20px;
cursor: pointer;
}
form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.private-container {
margin-left: 10px;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
input:disabled {
background-color: black;
color: #999;
border: 1px solid #ddd;
}

View File

@ -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 (
<div className='lp-container'>
<div className='title-container'>
<h1>Host A Server</h1>
</div>
<div className='content-container'>
<form>
<input
type='text'
value={serverName}
onChange={e => setServerName(e.target.value)}
placeholder='Server Name'
/>
<div className='private-container'>
<p>Private</p>
<Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} />
</div>
<input
type='text'
value={password}
onChange={e => setPassword(e.target.value)}
placeholder='password'
disabled={!isPrivate}
/>
<button onClick={handleButtonClick}>Host</button>
</form>
</div>
</div>
);
};
export default AddServer;

View File

@ -0,0 +1,53 @@
.login-page-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
height: 50%;
width: 100%;
}
.lp-container {
margin: 50px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 500px;
padding: 20px;
color: white;
}
.title-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
height: 40px;
border: 1px solid white;
border-radius: 2px;
transform: scaleX(1.02);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
padding-left: 10px;
text-align: center;
}
.title-container > h1 {
width: 100%;
margin: 0;
padding: 0;
}
.content-container {
display: flex;
flex-direction: column;
width: 100%;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-left: 1px solid black;
border-right: 1px solid black;
border-bottom: 1px solid black;
}

View File

@ -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 (
<>
<div className='login-page-container'>
<ServerList />
<AddServer />
</div>
</>
);
};
export default LoginPage;

View File

@ -0,0 +1,70 @@
.name-input-container {
display: flex;
flex-direction: column;
padding: 10px 20px 60px 20px;
width: 300px;
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
border: 1px solid white;
border-radius: 8px;
margin: 20px;
}
.name-input-container > button {
margin-top: 5px;
text-align: center;
width: 100px;
align-self: center;
}
.name-input-container > input {
margin-top: 10px;
}
.name-overlay {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
input,
button {
padding: 0;
border: none;
outline: none;
box-sizing: border-box;
}
input {
width: 100%;
padding: 12px;
font-size: 16px;
border-radius: 8px;
color: white;
border: 1px solid #ccc;
background-color: rgba(0, 0, 0, 0.2);
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
}
input:focus {
color: black;
border-color: #4a90e2;
background-color: #fff;
}
button {
padding: 12px 20px;
font-size: 16px;
border-radius: 8px;
border: none;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
button:hover {
background-color: rgba(0, 0, 0, 1);
}

View File

@ -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 (
<div className='name-overlay'>
<div className='name-input-container' style={{ height: isRoomPrivate ? '100px' : '50px' }}>
<input placeholder='Nickname' type='text' onChange={nickname.onChange} />
{isRoomPrivate ? (
<input
placeholder='Room password'
type='text'
onChange={password.onChange}
style={{ backgroundColor: isPasswordWrong ? 'red' : null }}
/>
) : null}
<button onClick={handleButtonClick}>JOIN</button>
</div>
</div>
);
};
export default NameInput;

View File

@ -0,0 +1,51 @@
th {
text-align: left;
}
img {
margin-right: 5px;
width: 20px;
height: 20px;
}
th,
td {
padding: 8px;
text-align: left;
height: 50px;
}
tr {
max-height: 50px;
}
table {
border-collapse: collapse;
width: 100%;
}
.server-container {
display: flex;
height: 500px;
overflow: scroll;
}
.room-name {
max-width: 150px;
overflow: hidden;
}
/* Firefox */
* {
scrollbar-width: auto;
scrollbar-color: #ffffff rgba(0, 0, 0, 0.1);
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
background: rgba(0, 0, 0, 0);
width: 10px;
}
*::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0);
}
*::-webkit-scrollbar-thumb {
background-color: #ffffff;
border-radius: 10px;
}

View File

@ -0,0 +1,77 @@
import React, { useContext, useEffect, useState } from 'react';
import { SocketContext } from '../../../App';
import lock from '../../../images/login-page/lock.png';
import refresh from '../../../images/login-page/refresh.png';
import ReactLoading from 'react-loading';
import './ServerList.css';
import NameInput from '../NameInput/NameInput';
const ServerList = () => {
const socket = useContext(SocketContext);
const [rooms, setRooms] = useState([]);
const [joining, setJoining] = useState(false);
const [clickedRoom, setClickedRoom] = useState(null);
useEffect(() => {
socket.emit('room:rooms');
socket.on('room:rooms', data => {
data = JSON.parse(data);
setRooms(data);
});
}, []);
const getRooms = () => {
setRooms(null);
socket.emit('room:rooms');
};
const handleJoinClick = room => {
setClickedRoom(room);
setJoining(true);
};
return (
<div className='lp-container'>
<div className='title-container'>
<h1>Server List</h1>
<div className='refresh'>
<img src={refresh} onClick={getRooms}></img>
</div>
</div>
<div className='server-container content-container'>
{rooms ? (
<table className='rooms'>
<thead>
<tr>
<th></th>
<th>Server</th>
<th>#/#</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{rooms.map((room, index) => (
<tr key={index}>
<td>{room.private ? <img src={lock} /> : null}</td>
<td className='room-name'>{room.name}</td>
<td>{`${room.players.length}/4`}</td>
<td>{room.isStarted ? 'started' : 'waiting'}</td>
<td>
<button onClick={() => handleJoinClick(room)}>Join</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div style={{ alignSelf: 'center' }}>
<ReactLoading type='spinningBubbles' color='white' height={50} width={50} />
</div>
)}
</div>
{joining ? <NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} /> : null}
</div>
);
};
export default ServerList;

View File

@ -1,25 +0,0 @@
import React, { useState, useContext } from "react";
import { SocketContext } from "../App";
const NameInput = () => {
const socket = useContext(SocketContext);
const [inputValue, setInputValue] = useState("");
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleButtonClick = () => {
socket.emit("player:login", { name: inputValue });
};
return (
<div>
<input
placeholder="Enter name"
type="text"
onChange={handleInputChange}
/>
<input type="submit" onClick={handleButtonClick} />
</div>
);
};
export default NameInput;

View File

@ -1,20 +1,16 @@
import React, { useState, useEffect } from 'react'; import React, { useMemo } from 'react';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import './TimerAnimation.js'; import './TimerAnimation.js';
const AnimatedOverlay = ({ time }) => { const AnimatedOverlay = ({ time }) => {
const [animationDelay, setAnimationDelay] = useState(); const animationDelay = useMemo(() => 15 - Math.ceil((time - Date.now()) / 1000), [time]);
useEffect(() => {
setAnimationDelay(15 - Math.ceil((time - Date.now()) / 1000));
}, [time]);
return ( return (
<CSSTransition <CSSTransition
in={true} in={true}
timeout={0} timeout={0}
classNames='overlay' classNames='overlay'
style={{ 'animation-delay': `-${animationDelay}s` }} style={{ animationDelay: `-${animationDelay}s` }}
unmountOnExit unmountOnExit
> >
<div className='overlay'></div> <div className='overlay'></div>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import AnimatedOverlay from './AnimatedOverlay'; import AnimatedOverlay from './AnimatedOverlay/AnimatedOverlay';
const NameContainer = ({ player, time }) => { const NameContainer = ({ player, time }) => {
return ( return (

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import Dice from './game-board-components/Dice'; import Dice from '../Gameboard/Dice/Dice';
import NameContainer from './navbar-components/NameContainer'; import NameContainer from './NameContainer/NameContainer';
import ReadyButton from './navbar-components/ReadyButton'; import ReadyButton from './ReadyButton/ReadyButton';
import './Navbar.css'; import './Navbar.css';
import { useContext } from 'react'; import { useContext } from 'react';
import { PlayerDataContext } from '../App'; import { PlayerDataContext } from '../../App';
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => { const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => {
const context = useContext(PlayerDataContext); const context = useContext(PlayerDataContext);
const colors = ['red', 'blue', 'green', 'yellow']; const colors = ['red', 'blue', 'green', 'yellow'];

View File

@ -1,19 +1,17 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { SocketContext } from '../../App'; import { SocketContext } from '../../../App';
import Switch from '@material-ui/core/Switch'; import Switch from '@material-ui/core/Switch';
import '../Navbar.css'; import '../Navbar.css';
import './TimerAnimation'; import '../NameContainer/AnimatedOverlay/TimerAnimation';
const ReadyButton = ({ isReady }) => { const ReadyButton = ({ isReady }) => {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
const [checked, setChecked] = useState(); const [checked, setChecked] = useState(isReady);
const handleCheckboxChange = () => { const handleCheckboxChange = () => {
socket.emit('player:ready'); socket.emit('player:ready');
setChecked(!checked); setChecked(!checked);
}; };
useEffect(() => {
setChecked(isReady);
});
return ( return (
<div className='ready-container'> <div className='ready-container'>
<Switch onChange={handleCheckboxChange} checked={checked || false} /> <Switch onChange={handleCheckboxChange} checked={checked || false} />

View File

@ -1,230 +0,0 @@
import React, { useEffect, useRef, useState, useContext, useCallback } from 'react';
import { PlayerDataContext, SocketContext } from '../../App';
import positions from './positions';
import bluePawn from '../../images/pawns/blue-pawn.png';
import greenPawn from '../../images/pawns/green-pawn.png';
import yellowPawn from '../../images/pawns/yellow-pawn.png';
import redPawn from '../../images/pawns/red-pawn.png';
import greyPawn from '../../images/pawns/grey-pawn.png';
const Map = ({ pawns, nowMoving, rolledNumber }) => {
const context = useContext(PlayerDataContext);
const socket = useContext(SocketContext);
const [hintPawn, setHintPawn] = useState();
const paintPawn = (context, x, y, color) => {
const circle = new Path2D();
circle.arc(x, y, 12, 0, 2 * Math.PI);
const image = new Image();
switch (color) {
case 'green':
image.src = greenPawn;
break;
case 'blue':
image.src = bluePawn;
break;
case 'red':
image.src = redPawn;
break;
case 'yellow':
image.src = yellowPawn;
break;
case 'grey':
image.src = greyPawn;
break;
}
context.drawImage(image, x - 17, y - 14, 35, 30);
return circle;
};
const canvasRef = useRef(null);
// Return true when pawn can move
const checkIfPawnCanMove = useCallback(
pawn => {
// If is in base
if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) {
return true;
// Other situations: pawn is on map or pawn is in end positions
} else if (pawn.position !== pawn.basePos) {
switch (pawn.color) {
case 'red':
if (pawn.position + rolledNumber <= 73) return true;
break;
case 'blue':
if (pawn.position + rolledNumber <= 79) return true;
break;
case 'green':
if (pawn.position + rolledNumber <= 85) return true;
break;
case 'yellow':
if (pawn.position + rolledNumber <= 91) return true;
break;
default:
return false;
}
} else {
return false;
}
},
[rolledNumber]
);
const handleCanvasClick = event => {
// If hint pawn exist it means that pawn can move
if (hintPawn) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect(),
x = event.clientX - rect.left,
y = event.clientY - rect.top;
for (const pawn of pawns) {
if (ctx.isPointInPath(pawn.circle, x, y)) {
socket.emit('game:move', pawn._id);
}
}
setHintPawn(null);
}
};
const getHintPawnPosition = pawn => {
// Based on color (because specific color have specific base and end positions)
let { position } = pawn;
switch (context.color) {
case 'red':
// When in base
if (position >= 0 && position <= 3) {
return 16;
// Next to end
} else if (position <= 66 && position + rolledNumber >= 67) {
return position + rolledNumber + 1; // 1 is difference between last position on map and first on end
// Normal move
} else {
return position + rolledNumber;
}
case 'blue':
// When in base
if (position >= 4 && position <= 7) {
return 55;
// Next to red base
} else if (position <= 67 && position + rolledNumber > 67) {
return position + rolledNumber - 52;
// Next to base
} else if (position <= 53 && position + rolledNumber >= 54) {
return position + rolledNumber + 20;
// Normal move
} else {
return position + rolledNumber;
}
case 'green':
// When in base
if (position >= 8 && position <= 11) {
return 42;
// Next to red base
} else if (position <= 67 && position + rolledNumber > 67) {
return position + rolledNumber - 52;
// Next to base
} else if (position <= 40 && position + rolledNumber >= 41) {
return position + rolledNumber + 39;
// Normal move
} else {
return position + rolledNumber;
}
case 'yellow':
// When in base
if (position >= 12 && position <= 15) {
return 29;
// Next to red base
} else if (position <= 67 && position + rolledNumber > 67) {
return position + rolledNumber - 52;
// Next to base
} else if (position <= 27 && position + rolledNumber >= 28) {
return position + rolledNumber + 58;
// Normal move
} else {
return position + rolledNumber;
}
default:
return position;
}
};
const handleMouseMove = event => {
if (nowMoving && rolledNumber) {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// Gets x and y cords of mouse on canvas
const rect = canvas.getBoundingClientRect(),
x = event.clientX - rect.left,
y = event.clientY - rect.top;
canvas.style.cursor = 'default';
for (const pawn of pawns) {
if (pawn.circle) {
/*
This condition checks if mouse location is:
1) on pawn
2) is color of pawn same as player's
3) if pawn can move
And then sets cursor to pointer and paints hint pawn - where will be pawn after click
*/
if (
ctx.isPointInPath(pawn.circle, x, y) &&
context.color === pawn.color &&
checkIfPawnCanMove(pawn)
) {
const pawnPosition = getHintPawnPosition(pawn);
// Checks if pawn can make a move
if (pawnPosition) {
canvas.style.cursor = 'pointer';
setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' });
break;
}
} else {
setHintPawn(null);
}
}
}
}
};
const rerenderCanvas = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const image = new Image();
image.src = 'https://img-9gag-fun.9cache.com/photo/a8GdpYZ_460s.jpg';
image.onload = function () {
ctx.drawImage(image, 0, 0);
pawns.forEach((pawn, index) => {
pawns[index].circle = paintPawn(
ctx,
positions[pawn.position].x,
positions[pawn.position].y,
pawn.color
);
});
if (hintPawn) {
paintPawn(ctx, positions[hintPawn.position].x, positions[hintPawn.position].y, hintPawn.color);
}
};
}, [checkIfPawnCanMove, context.color, hintPawn, nowMoving, pawns, rolledNumber]);
// Rerender canvas when pawns have changed
useEffect(() => {
rerenderCanvas();
}, [hintPawn, pawns, rerenderCanvas]);
useEffect(() => {
socket.on('game:move', () => {
setHintPawn(null);
});
socket.on('game:roll', () => {
setHintPawn(null);
});
}, [socket]);
return (
<canvas
className='canvas-container'
width={460}
height={460}
ref={canvasRef}
onClick={handleCanvasClick}
onMouseMove={handleMouseMove}
/>
);
};
export default Map;

View File

@ -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,
};

117
src/constants/positions.js Normal file
View File

@ -0,0 +1,117 @@
const positions = [
// Red base
{ x: 67, y: 67 }, // 0
{ x: 67, y: 116 },
{ x: 117, y: 67 },
{ x: 117, y: 116 },
// Blue base
{ x: 67, y: 343 },
{ x: 67, y: 392 },
{ x: 117, y: 343 },
{ x: 117, y: 392 },
// Green base
{ x: 343, y: 343 },
{ x: 392, y: 392 },
{ x: 392, y: 343 }, // 10
{ x: 343, y: 392 },
// Yellow base
{ x: 343, y: 67 },
{ x: 392, y: 116 },
{ x: 392, y: 67 },
{ x: 343, y: 116 },
// Map - starting from red field
{ x: 45, y: 200 },
{ x: 76, y: 200 },
{ x: 107, y: 200 },
{ x: 138, y: 200 },
{ x: 169, y: 200 }, // 20
{ x: 200, y: 169 },
{ x: 200, y: 138 },
{ x: 200, y: 107 },
{ x: 200, y: 76 },
{ x: 200, y: 45 },
{ x: 200, y: 14 },
// Top
{ x: 230, y: 14 },
{ x: 261, y: 14 },
{ x: 261, y: 45 },
{ x: 261, y: 76 }, // 30
{ x: 261, y: 107 },
{ x: 261, y: 138 },
{ x: 261, y: 169 },
{ x: 291, y: 200 },
{ x: 321, y: 200 },
{ x: 352, y: 200 },
{ x: 383, y: 200 },
{ x: 414, y: 200 },
{ x: 445, y: 200 },
// Right
{ x: 445, y: 230 }, // 40
{ x: 445, y: 261 },
{ x: 414, y: 261 },
{ x: 383, y: 261 },
{ x: 352, y: 261 },
{ x: 321, y: 261 },
{ x: 291, y: 261 },
{ x: 261, y: 291 },
{ x: 261, y: 322 },
{ x: 261, y: 353 },
{ x: 261, y: 384 }, // 50
{ x: 261, y: 414 },
{ x: 261, y: 445 },
// Bottom
{ x: 230, y: 445 },
{ x: 200, y: 445 },
{ x: 200, y: 414 },
{ x: 200, y: 384 },
{ x: 200, y: 353 },
{ x: 200, y: 322 },
{ x: 200, y: 291 },
{ x: 169, y: 261 }, // 60
{ x: 138, y: 261 },
{ x: 107, y: 261 },
{ x: 76, y: 261 },
{ x: 45, y: 261 },
{ x: 15, y: 261 },
// Left
{ x: 15, y: 231 }, // 66
// One behind red base
{ x: 15, y: 200 }, //67
// Red end
{ x: 45, y: 231 }, // 68
{ x: 76, y: 231 },
{ x: 107, y: 231 },
{ x: 138, y: 231 },
{ x: 169, y: 231 },
{ x: 200, y: 231 }, // 73
// Blue end
{ x: 231, y: 414 }, // 74
{ x: 231, y: 384 },
{ x: 231, y: 353 },
{ x: 231, y: 322 },
{ x: 231, y: 291 },
{ x: 231, y: 260 }, // 79
// Green end
{ x: 414, y: 231 }, // 80
{ x: 383, y: 231 },
{ x: 352, y: 231 },
{ x: 321, y: 231 },
{ x: 290, y: 231 },
{ x: 259, y: 231 }, // 85
// Yellow base
{ x: 230, y: 45 }, // 86
{ x: 230, y: 76 },
{ x: 230, y: 107 },
{ x: 230, y: 138 },
{ x: 230, y: 169 },
{ x: 230, y: 200 }, // 91
];
export default positions;

11
src/hooks/useInput.js Normal file
View File

@ -0,0 +1,11 @@
import { useState } from 'react';
export default function useInput({ initialValue }) {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
};
return {
value,
onChange: handleChange,
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
src/images/map.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB