diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9ae6313 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "files.associations": { + "*.tcc": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cstdint": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "exception": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "ostream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "system_error": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp" + } +} \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 3343cfe..59a5613 100644 --- a/backend/app.js +++ b/backend/app.js @@ -4,6 +4,7 @@ const cors = require("cors"); const authRoutes = require("./routes/auth"); const userRoutes = require("./routes/user"); const mongoose = require("mongoose"); +require("dotenv").config(); mongoose .connect("mongodb://127.0.0.1:27017/test") @@ -14,27 +15,65 @@ const app = express(); const http = require("http"); const server = http.createServer(app); const { Server } = require("socket.io"); +const { sendEmail } = require("./mail"); +const { User } = require("./models/user"); const io = new Server(server, { cors: { origin: "*" } }); -io.on("connection", (socket) => { - // console.log("Connected: ", socket.data); - socket.broadcast.emit("game", "emitting..."); - socket.emit("game", "Welcome"); +const activeRooms = new Map(); +const pendingChallenges = new Map(); - socket.on("join-room", async (data) => { - if (data?.roomid) await socket.join(roomid); - else socket.emit("error", "Room id not received"); +io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + // Handle join event + socket.on("join-room", async (roomID, playerUsername, challengedPlayerUsername) => { + // room exists + if (activeRooms.has(roomID)) { + socket.emit("room-full"); + return; + } else if (pendingChallenges.has(roomID)) { + const challenge = pendingChallenges.get(roomID); + pendingChallenges.delete(roomID); + let newRoom = { + id: roomID, + players: { + challenger: { + id: challenge.challengerID, + name: challenge.challengerUsername, + }, + challenged: { + id: socket.id, + name: playerUsername, + }, + }, + }; + activeRooms.set(roomID, newRoom); + io.to(roomID).emit("room-created"); + } else { + // no room on pending challenges found + const challenge = { + roomID, + challengerID: socket.id, + challengerUsername: playerUsername, + challengedUsername: challengedPlayerUsername, + }; + pendingChallenges.set(roomID, challenge); + + console.log(challengedPlayerUsername); + // notify the challenged player + const email = (await User.findOne({ username: challengedPlayerUsername })).email; + sendEmail( + email, + `Challenge from ${playerUsername}`, + `To accept the challenge follow the link: http://localhost:5173/game/friend/${roomID}` + ); + + socket.emit("challenge-pending"); + } }); - socket.on("play", (data) => { - console.log(data); - let { fromRow, fromCol, toRow, toCol, room } = data; - fromRow = 7 - fromRow; - fromCol = 7 - fromCol; - toRow = 7 - toRow; - toCol = 7 - toCol; - socket.to(room).emit("play", { fromCol, fromRow, toCol, toRow }); - socket.broadcast.emit("play", { fromCol, fromRow, toCol, toRow }); + socket.on("move", (roomID, moveData) => { + socket.to(roomID).emit("opponent-move", moveData); }); }); diff --git a/backend/events.json b/backend/events.json deleted file mode 100644 index 210b635..0000000 --- a/backend/events.json +++ /dev/null @@ -1 +0,0 @@ -{"events":[{"title":"new event","image":"https://images.squarespace-cdn.com/content/v1/56d082a760b5e95c074d11a0/8ce30633-23f2-4dc9-b301-c251cbf26d02/Mercedes+Benz+Oscar+Party?format=2500w","date":"2023-05-03","description":"hello","id":"1f24eb47-5019-4a07-9c3c-1ba18b591800"},{"title":"concert","image":"https://business.twitter.com/content/dam/business-twitter/insights/may-2018/event-targeting.png.twimg.1920.png","date":"2023-05-04","description":"lorem","id":"2035d991-0e72-456c-b6df-1aa9774caaaa"}],"users":[{"email":"m@g.com","password":"$2a$12$.GW99UfbOfISG6mQHN.8MOXtYXePsWH592XudNWU9Df9tYEV.K.QG","id":"2a744c6d-da1a-4938-ab45-144b567d5187"},{"email":"moon@gmails.com","password":"$2a$12$o7p.OS1vA66yBlkOsh.xV.ifDsItbSeBOTaN5VgjYHIUVtDvP3nbu","id":"8f394056-592e-45fc-9808-e923e1a1d60a"},{"email":"moon@gmail.com","password":"$2a$12$3t8sENEvXNvC1XeKhMsJFOsaU0vVh27wnY4hHIPlXzzOIAwMiSSJC","id":"421e3728-ac58-4fc1-86d5-70b618d01f9b"},{"email":"moonpatel2003@gmail.com","password":"$2a$12$QxG6hh0vFudhlsX3MJvXLuVfnFKBAPd15Bei42MNB2v69BwwGT3oq","id":"d77a0a0b-2b82-489e-9782-ccac490db36d"}]} \ No newline at end of file diff --git a/backend/mail.js b/backend/mail.js new file mode 100644 index 0000000..8cfa8f8 --- /dev/null +++ b/backend/mail.js @@ -0,0 +1,29 @@ +const nodemailer = require("nodemailer"); +const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: "chessroyalemail@gmail.com", + pass: process.env.MAIL_SERVER_PASSWORD, + }, +}); + +const sendEmail = (receiverEmail, subject, data) => { + console.log(process.env.MAIL_SERVER_PASSWORD); + let mailDetails = { + from: "chessroyalemail@gmail.com", + to: receiverEmail, + subject: subject, + text: data, + }; + transporter.sendMail(mailDetails, function (err, data) { + if (err) { + console.log(err); + } else { + console.log("Email sent successfully"); + } + }); +}; + +module.exports = { + sendEmail, +}; diff --git a/backend/models/user.js b/backend/models/user.js index 26f7e91..f467b4d 100644 --- a/backend/models/user.js +++ b/backend/models/user.js @@ -39,7 +39,7 @@ const userSchema = new Schema( methods: { async getFriends() { await this.populate("friends", "username"); - console.log(this.friends); + // console.log(this.friends); return this.friends.map(friend => friend.username); }, }, diff --git a/backend/package-lock.json b/backend/package-lock.json index 4cdb6cc..67d84dc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,9 +12,11 @@ "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^8.5.1", "mongoose": "^7.2.1", + "nodemailer": "^6.9.3", "socket.io": "^4.6.1", "uuid": "^9.0.0" } @@ -211,6 +213,17 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -774,6 +787,14 @@ "node": ">= 0.6" } }, + "node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 4330c71..10a5959 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,9 +14,11 @@ "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^8.5.1", "mongoose": "^7.2.1", + "nodemailer": "^6.9.3", "socket.io": "^4.6.1", "uuid": "^9.0.0" } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7d74cda..b40262d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,8 @@ "@tabler/icons-react": "^2.23.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.14.0" + "react-router-dom": "^6.14.0", + "socket.io-client": "^4.7.1" }, "devDependencies": { "@types/react": "^18.0.37", @@ -1273,6 +1274,11 @@ "node": ">=14" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@tabler/icons": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.23.0.tgz", @@ -1704,7 +1710,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1762,6 +1767,26 @@ "integrity": "sha512-r6dCgNpRhPwiWlxbHzZQ/d9swfPaEJGi8ekqRBwQYaR3WmA5VkqQfBWSDDjuJU1ntO+W9tHx8OHV/96Q8e0dVw==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.1.tgz", + "integrity": "sha512-hE5wKXH8Ru4L19MbM1GgYV/2Qo54JSMh1rlJbfpa40bEWkCKNo3ol2eOtGmowcr+ysgbI7+SGL+by42Q3pt/Ng==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.1.0", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", + "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3016,8 +3041,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.6", @@ -3687,6 +3711,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io-client": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.1.tgz", + "integrity": "sha512-Qk3Xj8ekbnzKu3faejo4wk2MzXA029XppiXtTF/PkbTg+fcwaTw1PlDrTrrrU4mKoYC4dvlApOnSeyLCKwek2w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4127,6 +4177,34 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5c5b2f5..875c587 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,8 @@ "@tabler/icons-react": "^2.23.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.14.0" + "react-router-dom": "^6.14.0", + "socket.io-client": "^4.7.1" }, "devDependencies": { "@types/react": "^18.0.37", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 460419e..123da4f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -17,8 +17,8 @@ import Play from './pages/Play/Play' import AuthenticationPage, { loginAction, signupAction } from './pages/Authentication/Authentication' import { getAuthToken } from '../utils/auth' import { logoutAction } from './components/Logout' - - +import ChallengeFriend, { playFriendAction } from './pages/Play/ChallengeFriend' +import ChessGame from './pages/Chess/ChessGame' const router = createBrowserRouter([{ path: '/', @@ -30,11 +30,13 @@ const router = createBrowserRouter([{ { path: 'play', element: , children: [ { index: true, element: }, + { path: 'friend/:friend_username', element: , action: playFriendAction }, { path: 'friend', element: }, { path: 'computer', element:
Computer
}, { path: 'online', element:
Online
} ] }, + { path: "game/friend/:roomID", element: }, { path: 'settings', element: , children: [ { index: true, element: }, diff --git a/frontend/src/assets/bishop_black.png b/frontend/src/assets/bishop_black.png new file mode 100644 index 0000000..df2e407 Binary files /dev/null and b/frontend/src/assets/bishop_black.png differ diff --git a/frontend/src/assets/bishop_white.png b/frontend/src/assets/bishop_white.png new file mode 100644 index 0000000..5e26a38 Binary files /dev/null and b/frontend/src/assets/bishop_white.png differ diff --git a/frontend/src/assets/king_black.png b/frontend/src/assets/king_black.png new file mode 100644 index 0000000..35637f5 Binary files /dev/null and b/frontend/src/assets/king_black.png differ diff --git a/frontend/src/assets/king_white.png b/frontend/src/assets/king_white.png new file mode 100644 index 0000000..44ded88 Binary files /dev/null and b/frontend/src/assets/king_white.png differ diff --git a/frontend/src/assets/knight_black.png b/frontend/src/assets/knight_black.png new file mode 100644 index 0000000..e79df05 Binary files /dev/null and b/frontend/src/assets/knight_black.png differ diff --git a/frontend/src/assets/knight_white.png b/frontend/src/assets/knight_white.png new file mode 100644 index 0000000..e382675 Binary files /dev/null and b/frontend/src/assets/knight_white.png differ diff --git a/frontend/src/assets/pawn_black.png b/frontend/src/assets/pawn_black.png new file mode 100644 index 0000000..bb3cfdf Binary files /dev/null and b/frontend/src/assets/pawn_black.png differ diff --git a/frontend/src/assets/pawn_white.png b/frontend/src/assets/pawn_white.png new file mode 100644 index 0000000..6d1b1fe Binary files /dev/null and b/frontend/src/assets/pawn_white.png differ diff --git a/frontend/src/assets/queen_black.png b/frontend/src/assets/queen_black.png new file mode 100644 index 0000000..a2ed748 Binary files /dev/null and b/frontend/src/assets/queen_black.png differ diff --git a/frontend/src/assets/queen_white.png b/frontend/src/assets/queen_white.png new file mode 100644 index 0000000..bdc6ff3 Binary files /dev/null and b/frontend/src/assets/queen_white.png differ diff --git a/frontend/src/assets/rook_black.png b/frontend/src/assets/rook_black.png new file mode 100644 index 0000000..722db00 Binary files /dev/null and b/frontend/src/assets/rook_black.png differ diff --git a/frontend/src/assets/rook_white.png b/frontend/src/assets/rook_white.png new file mode 100644 index 0000000..49b7d0f Binary files /dev/null and b/frontend/src/assets/rook_white.png differ diff --git a/frontend/src/assets/sprites.png b/frontend/src/assets/sprites.png new file mode 100644 index 0000000..f2d3137 Binary files /dev/null and b/frontend/src/assets/sprites.png differ diff --git a/frontend/src/components/Cell.jsx b/frontend/src/components/Cell.jsx new file mode 100644 index 0000000..381f79d --- /dev/null +++ b/frontend/src/components/Cell.jsx @@ -0,0 +1,44 @@ +import React from 'react' +import Piece from './Piece'; +import { socket } from '../socket'; +import { Box, Flex } from '@mantine/core'; + +const Cell = ({ cellProps, selectedPiece, dispatch, myColor }) => { + const { row, col, piece, marked } = cellProps; + let bgColor = (selectedPiece?.row === row && selectedPiece?.col === col) ? 'bg-gray-100' : ((row + col) % 2 ? 'bg-stone-800' : 'bg-neutral-200'); + bgColor = marked && piece ? `bg-red-300` : bgColor; + + const handleClick = () => { + // if (!myTurn) return; + if (piece && piece.color === myColor) { + dispatch({ type: 'SELECT_PIECE', val: { row, col, color: piece.color } }) // select piece + } + else if (!piece && selectedPiece && marked) { + let payload = { fromRow: selectedPiece.row, fromCol: selectedPiece.col, toRow: row, toCol: col }; + socket.emit('move', payload); + dispatch({ type: 'MOVE_PIECE', val: payload, }); // move piece + } + else if (piece && marked && piece.color !== myColor) { + let payload = { fromRow: selectedPiece.row, fromCol: selectedPiece.col, toRow: row, toCol: col, color: piece.color }; + socket.emit('move', payload); + dispatch({ type: 'CAPTURE_PIECE', val: payload }) // capture piece + } + } + + const content = marked ? : ; + + return ( + + {content} + + ) +} + +export const Mark = () => { + return ( + + ) +} + + +export default Cell \ No newline at end of file diff --git a/frontend/src/components/FriendsList.jsx b/frontend/src/components/FriendsList.jsx new file mode 100644 index 0000000..829939d --- /dev/null +++ b/frontend/src/components/FriendsList.jsx @@ -0,0 +1,53 @@ +import { Avatar, Flex, Image, Loader, NavLink, Text, Title } from '@mantine/core'; +import { Link } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; + +const FriendsList = () => { + const [friends, setFriends] = useState(null); + useEffect(() => { + const abortController = new AbortController(); + let response = null; + + const fetchData = async () => { + let url = `${import.meta.env.VITE_BACKEND_HOST}/api/user/user1/friends`; + try { + response = await fetch(url, { signal: abortController.signal }); + const data = await response.json(); + if (data.success) { + setFriends(data.friends); + } else { + throw data; + } + } catch (error) { + if (error.message === 'The user aborted a request.'); + else { + console.log('Error fetching data'); + throw error; + } + } + } + + fetchData(); + + return () => { + if (!response) { + abortController.abort(); + } + } + }, []); + + return ( + + Friends + + { + friends ? + friends.map((friend, index) => } label={{friend}} />) + : + + } + + ) +} + +export default FriendsList \ No newline at end of file diff --git a/frontend/src/components/Piece.jsx b/frontend/src/components/Piece.jsx new file mode 100644 index 0000000..6833801 --- /dev/null +++ b/frontend/src/components/Piece.jsx @@ -0,0 +1,33 @@ +import { Image } from '@mantine/core'; +import React from 'react'; + +const Piece = ({ piece }) => { + if (piece === null) return null; + const { type, color } = piece; + let logo; + switch (type) { + case 'P': + logo = color === 'W' ? 'pawn_white' : 'pawn_black'; + break; + case 'R': + logo = color === 'W' ? 'rook_white' : 'rook_black'; + break; + case 'N': + logo = color === 'W' ? 'knight_white' : 'knight_black'; + break; + case 'B': + logo = color === 'W' ? 'bishop_white' : 'bishop_black'; + break; + case 'Q': + logo = color === 'W' ? 'queen_white' : 'queen_black'; + break; + case 'K': + logo = color === 'W' ? 'king_white' : 'king_black'; + break; + } + return ( + + ) +} + +export default Piece \ No newline at end of file diff --git a/frontend/src/pages/Authentication/Authentication.jsx b/frontend/src/pages/Authentication/Authentication.jsx index 721837a..c649932 100644 --- a/frontend/src/pages/Authentication/Authentication.jsx +++ b/frontend/src/pages/Authentication/Authentication.jsx @@ -8,7 +8,7 @@ const AuthenticationPage = (props) => { const { isLogin } = props; return ( - + ({ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', backgroundImage: theme.fn.gradient({ from: 'blue', to: 'teal' }) })}> {isLogin ? 'Login' : 'Sign Up'} diff --git a/frontend/src/pages/Chess/ChessGame.jsx b/frontend/src/pages/Chess/ChessGame.jsx new file mode 100644 index 0000000..cc40e38 --- /dev/null +++ b/frontend/src/pages/Chess/ChessGame.jsx @@ -0,0 +1,30 @@ +import { Avatar, Flex, Image, NavLink, Text, Title } from '@mantine/core' +import React from 'react' +import ChessBoard from '../Play/ChessBoard' + +const ChessGame = () => { + return ( + + + } + description={"description"} + /> + + } + description={"description"} + /> + + + Game Data + + + ) +} + +export default ChessGame \ No newline at end of file diff --git a/frontend/src/pages/Play/ChallengeFriend.jsx b/frontend/src/pages/Play/ChallengeFriend.jsx new file mode 100644 index 0000000..071131c --- /dev/null +++ b/frontend/src/pages/Play/ChallengeFriend.jsx @@ -0,0 +1,49 @@ +import { Avatar, Button, Card, Flex, Image, Select, Text, TextInput, Title } from '@mantine/core' +import React from 'react' +import FriendsList from '../../components/FriendsList' +import { IconSearch } from '@tabler/icons-react' +import { Form, Link, redirect, useParams, useSearchParams } from 'react-router-dom' +import { socket } from '../../socket' + +const ChallengeFriend = () => { + const params = useParams(); + let friend_username = params["friend_username"]; + + return ( + + + Play vs {friend_username} + {friend_username[0].toUpperCase()} + {friend_username} + + setColor(evt.target.value)} my="20px" label={I play as} placeholder='choose your color' data={[ + { value: 'W', label: 'White' }, + { value: 'B', label: 'Black' }, + { value: 'RANDOM', label: 'Random' } + ]} /> +
+ +
+
+ ) +} + +export const playFriendAction = ({ request, params }) => { + let roomID = Math.floor(Math.random() * 1_000_000_000).toString(); + // const req = new URL(request); + socket.connect(); + socket.emit('join-room', roomID, 'user1', params.friend_username); + + // socket.on('pending-challenge', () => { + // }) + return redirect(`/game/friend/${roomID}`); +} + +export default ChallengeFriend; \ No newline at end of file diff --git a/frontend/src/pages/Play/ChessBoard.jsx b/frontend/src/pages/Play/ChessBoard.jsx new file mode 100644 index 0000000..974ed2a --- /dev/null +++ b/frontend/src/pages/Play/ChessBoard.jsx @@ -0,0 +1,104 @@ +import React, { useEffect, useReducer, useRef } from 'react'; +import { blackColor, chessBoardInit, getPieceHint, pieces, whiteColor } from '../../../utils/chess'; +import Cell from '../../components/Cell'; +import { socket } from '../../socket'; +import { Flex } from '@mantine/core'; + +let myColor = 'W'; + +const reducer = (state, action) => { + if (state.capturedPieces.length && state.capturedPieces.at(-1).type === pieces.king) return state; + switch (action.type) { + case 'SELECT_PIECE': + { + let { row, col, color } = action.val; + let selectedPiece = { row, col, color }; + let possibleMoves = getPieceHint(state.chessBoard, { row, col, color, type: state.chessBoard[row][col].type }, color).movePos; + return { ...state, selectedPiece, possibleMoves }; + } + break; + case 'MOVE_PIECE': + { + let { fromRow, fromCol, toRow, toCol } = action.val; + let newChessBoard = state.chessBoard.map(row => row.slice()); + let piece = state.chessBoard[fromRow][fromCol]; + newChessBoard[toRow][toCol] = piece; + newChessBoard[fromRow][fromCol] = null; + return { ...state, chessBoard: newChessBoard, possibleMoves: [], selectedPiece: null, myTurn: !state.myTurn }; + } + break; + case 'CAPTURE_PIECE': + { + let { fromRow, fromCol, toRow, toCol } = action.val; + let newChessBoard = state.chessBoard.map(row => row.slice()); + let capturedPieces = [...state.capturedPieces, state.chessBoard[toRow][toCol]]; + newChessBoard[toRow][toCol] = state.chessBoard[fromRow][fromCol]; + newChessBoard[fromRow][fromCol] = null; + return { ...state, chessBoard: newChessBoard, capturedPieces, possibleMoves: [], selectedPiece: null, myTurn: !state.myTurn }; + } + break; + default: + return state; + } +} + +const ChessBoard = () => { + const [gameState, dispatch] = useReducer(reducer, { + chessBoard: chessBoardInit(myColor), selectedPiece: null, possibleMoves: [], capturedPieces: [], myTurn: myColor === whiteColor + }); + + const chessBoardRef = useRef(gameState.chessBoard); + chessBoardRef.current = gameState.chessBoard; + + useEffect(() => { + function handleOpponentMove(data) { + let { fromCol, fromRow, toCol, toRow } = data; + if (chessBoardRef.current[toRow][toCol] === null) { + console.log('Moving piece: ', data) + dispatch({ type: 'MOVE_PIECE', val: { fromRow, fromCol, toRow, toCol } }); + } else if (myColor === chessBoardRef.current[toRow][toCol].color) { + dispatch({ type: 'CAPTURE_PIECE', val: { fromRow, fromCol, toRow, toCol } }); + } + } + socket.on('move', handleOpponentMove) + + return () => { + socket.off('move', handleOpponentMove); + } + }, []); + + return ( + + +
+ {gameState.chessBoard.map((line, row) => { + return ( + + {line.map((cell, col) => { + let marked = null; + for (let k = 0; k < gameState.possibleMoves.length; k++) { + if (gameState.possibleMoves[k].row === row && gameState.possibleMoves[k].col === col) { + marked = true; + break; + } + } + let piece = cell ? { type: cell.type, color: cell.color } : null; + return + })} + + ) + })} +
+
+
+ ) +} + +export default ChessBoard \ No newline at end of file diff --git a/frontend/src/pages/Play/Play.jsx b/frontend/src/pages/Play/Play.jsx index b5dcf4a..cee76fc 100644 --- a/frontend/src/pages/Play/Play.jsx +++ b/frontend/src/pages/Play/Play.jsx @@ -6,7 +6,7 @@ const Play = () => { return ( @@ -28,7 +28,7 @@ const CardItem = ({ label, description, src, to }) => { label={label} icon={} description={description} - sx={{ backgroundColor: 'gray', borderRadius: '5px' }} + sx={{ backgroundColor: 'black', borderRadius: '5px' }} /> ) } diff --git a/frontend/src/pages/Play/PlayFriend.jsx b/frontend/src/pages/Play/PlayFriend.jsx index 318e5fe..3c57ac5 100644 --- a/frontend/src/pages/Play/PlayFriend.jsx +++ b/frontend/src/pages/Play/PlayFriend.jsx @@ -1,8 +1,8 @@ import React, { useState } from 'react' import { Button, Card, CopyButton, Flex, Group, Image, Modal, NativeSelect, NavLink, Select, Text, TextInput, Title } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' -import { IconSearch } from '@tabler/icons-react' -import { PersonIcon } from '@radix-ui/react-icons' +import { IconSearch, IconUserCircle } from '@tabler/icons-react' +import FriendsList from '../../components/FriendsList' const createChallengeLink = (color) => { let challengeLink = Math.floor(Math.random() * 100_000_000).toString(); @@ -46,10 +46,7 @@ const PlayFriend = () => { Play a Friend } /> - - Friends - {friends.map((friend, index) => {friend.username}
} />)} - + @@ -60,10 +57,10 @@ const PlayFriend = () => { } const friends = [ - { avatar: , username: "friend", rating: 100 }, - { avatar: , username: "friend", rating: 100 }, - { avatar: , username: "friend", rating: 100 }, - { avatar: , username: "friend", rating: 100 }, + { avatar: , username: "friend", rating: 100 }, + { avatar: , username: "friend", rating: 100 }, + { avatar: , username: "friend", rating: 100 }, + { avatar: , username: "friend", rating: 100 }, ] export default PlayFriend \ No newline at end of file diff --git a/frontend/src/socket.js b/frontend/src/socket.js new file mode 100644 index 0000000..c8aaa64 --- /dev/null +++ b/frontend/src/socket.js @@ -0,0 +1,3 @@ +import { io } from "socket.io-client"; +const url = import.meta.env.VITE_BACKEND_HOST +export const socket = io(url, { autoConnect: false }); diff --git a/frontend/utils/chess.js b/frontend/utils/chess.js new file mode 100644 index 0000000..3f967dd --- /dev/null +++ b/frontend/utils/chess.js @@ -0,0 +1,305 @@ +const pawn = "P", + rook = "R", + knight = "N", + bishop = "B", + queen = "Q", + king = "K"; + +export const pieces = { + pawn, + rook, + knight, + bishop, + queen, + king, +}; + +export const whiteColor = "W", + blackColor = "B"; + +export function chessBoardInit(myColor) { + let opColor = myColor === whiteColor ? blackColor : whiteColor; + const chessBoardMatrix = [ + [ + { color: opColor, type: rook }, + { color: opColor, type: knight }, + { color: opColor, type: bishop }, + { color: opColor, type: myColor === whiteColor ? queen : king }, + { color: opColor, type: myColor === whiteColor ? king : queen }, + { color: opColor, type: bishop }, + { color: opColor, type: knight }, + { color: opColor, type: rook }, + ], + Array(8).fill({ color: opColor, type: pawn }), + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + Array(8).fill({ color: myColor, type: pawn }), + [ + { color: myColor, type: rook }, + { color: myColor, type: knight }, + { color: myColor, type: bishop }, + { color: myColor, type: myColor === whiteColor ? queen : king }, + { color: myColor, type: myColor === whiteColor ? king : queen }, + { color: myColor, type: bishop }, + { color: myColor, type: knight }, + { color: myColor, type: rook }, + ], + ]; + return chessBoardMatrix; +} + +function inBoard(i, j) { + if (i >= 0 && i < 8 && j >= 0 && j < 8) return true; + else return false; +} + +function isBlocked(chessBoard, chessPiece, i, j) { + if (chessBoard[i][j] === null) return false; + else if (chessBoard[i][j].color === chessPiece.color) return true; + else return false; +} + +function isAttacking(chessBoard, chessPiece, i, j) { + if (chessBoard[i][j] === null) return false; + else if (chessBoard[i][j].color !== chessPiece.color) return true; + else return false; +} + +function getPawnHint(chessBoard, chessPiece, myColor) { + const { row, col } = chessPiece; + let movePos = []; + if (chessPiece.color === myColor) { + // for moving forward + if (inBoard(row - 1, col) && chessBoard[row - 1][col] === null && movePos.push({ row: row - 1, col })) { + chessPiece.row === 6 && + chessBoard[row - 2][col] === null && + inBoard(row - 2, col) && + !isBlocked(chessBoard, chessPiece, row - 1, col) && + movePos.push({ row: row - 2, col }); + } + // for killing opponent piece + if ( + inBoard(row - 1, col + 1) && + chessBoard[row - 1][col + 1]?.type && + chessBoard[row - 1][col + 1]?.color !== myColor + ) + inBoard(row - 1, col + 1) && movePos.push({ row: row - 1, col: col + 1 }); + if ( + inBoard(row - 1, col - 1) && + chessBoard[row - 1][col - 1]?.type && + chessBoard[row - 1][col - 1]?.color !== myColor + ) + inBoard(row - 1, col - 1) && movePos.push({ row: row - 1, col: col - 1 }); + } else { + // for moving forward + if (inBoard(row + 1, col) && chessBoard[row + 1][col] === null && movePos.push({ row: row + 1, col })) { + chessPiece.row === 1 && + chessBoard[row + 2][col] === null && + inBoard(row + 2, col) && + movePos.push({ row: row + 2, col }); + } + // for killing opponent piece + if ( + inBoard(row + 1, col + 1) && + chessBoard[row + 1][col + 1]?.type && + chessBoard[row + 1][col + 1]?.color === myColor + ) + inBoard(row + 1, col + 1) && movePos.push({ row: row + 1, col: col + 1 }); + if ( + inBoard(row + 1, col - 1) && + chessBoard[row + 1][col - 1]?.type && + chessBoard[row + 1][col - 1]?.color === myColor + ) + inBoard(row + 1, col - 1) && movePos.push({ row: row + 1, col: col - 1 }); + } + + return { movePos }; +} + +function getRookHint(chessBoard, chessPiece, myColor) { + const { row, col, color } = chessPiece; + let movePos = []; + let i = row, + j = col; + while (inBoard(++i, j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(--i, j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(i, ++j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(i, --j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + console.log(movePos); + + return { movePos }; +} + +function getKnightHint(chessBoard, chessPiece, myColor) { + const { row, col, color } = chessPiece; + let movePos = []; + + if (inBoard(row + 2, col + 1) && !isBlocked(chessBoard, chessPiece, row + 2, col + 1)) + movePos.push({ row: row + 2, col: col + 1 }); + if (inBoard(row + 2, col - 1) && !isBlocked(chessBoard, chessPiece, row + 2, col - 1)) + movePos.push({ row: row + 2, col: col - 1 }); + if (inBoard(row - 2, col + 1) && !isBlocked(chessBoard, chessPiece, row - 2, col + 1)) + movePos.push({ row: row - 2, col: col + 1 }); + if (inBoard(row - 2, col - 1) && !isBlocked(chessBoard, chessPiece, row - 2, col - 1)) + movePos.push({ row: row - 2, col: col - 1 }); + if (inBoard(row + 1, col + 2) && !isBlocked(chessBoard, chessPiece, row + 1, col + 2)) + movePos.push({ row: row + 1, col: col + 2 }); + if (inBoard(row - 1, col + 2) && !isBlocked(chessBoard, chessPiece, row - 1, col + 2)) + movePos.push({ row: row - 1, col: col + 2 }); + if (inBoard(row + 1, col - 2) && !isBlocked(chessBoard, chessPiece, row + 1, col - 2)) + movePos.push({ row: row + 1, col: col - 2 }); + if (inBoard(row - 1, col - 2) && !isBlocked(chessBoard, chessPiece, row - 1, col - 2)) + movePos.push({ row: row - 1, col: col - 2 }); + + return { movePos }; +} + +function getBishopHint(chessBoard, chessPiece, myColor) { + const { row, col, color } = chessPiece; + let movePos = []; + + let i = row, + j = col; + while (inBoard(++i, ++j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(++i, --j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(--i, ++j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(--i, --j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + + return { movePos }; +} + +function getQueenHint(chessBoard, chessPiece, myColor) { + const { row, col, color } = chessPiece; + let movePos = []; + let i = row, + j = col; + while (inBoard(++i, j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(--i, j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(i, ++j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(i, --j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(++i, ++j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(++i, --j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(--i, ++j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + i = row; + j = col; + while (inBoard(--i, --j) && !isBlocked(chessBoard, chessPiece, i, j)) { + movePos.push({ row: i, col: j }); + if (isAttacking(chessBoard, chessPiece, i, j)) break; + } + + return { movePos }; +} + +function getKingHint(chessBoard, chessPiece, myColor) { + const { row, col } = chessPiece; + let movePos = []; + + if (inBoard(row, col + 1) && !isBlocked(chessBoard, chessPiece, row, col + 1)) + movePos.push({ row: row, col: col + 1 }); + if (inBoard(row, col - 1) && !isBlocked(chessBoard, chessPiece, row, col - 1)) + movePos.push({ row: row, col: col - 1 }); + if (inBoard(row + 1, col + 1) && !isBlocked(chessBoard, chessPiece, row + 1, col + 1)) + movePos.push({ row: row + 1, col: col + 1 }); + if (inBoard(row + 1, col) && !isBlocked(chessBoard, chessPiece, row + 1, col)) + movePos.push({ row: row + 1, col: col }); + if (inBoard(row + 1, col - 1) && !isBlocked(chessBoard, chessPiece, row + 1, col - 1)) + movePos.push({ row: row + 1, col: col - 1 }); + if (inBoard(row - 1, col + 1) && !isBlocked(chessBoard, chessPiece, row - 1, col + 1)) + movePos.push({ row: row - 1, col: col + 1 }); + if (inBoard(row - 1, col) && !isBlocked(chessBoard, chessPiece, row - 1, col)) + movePos.push({ row: row - 1, col: col }); + if (inBoard(row - 1, col - 1) && !isBlocked(chessBoard, chessPiece, row - 1, col - 1)) + movePos.push({ row: row - 1, col: col - 1 }); + + return { movePos }; +} + +export function getPieceHint(chessBoard, chessPiece, myColor) { + switch (chessPiece.type) { + case pawn: + return getPawnHint(chessBoard, chessPiece, myColor); + case rook: + return getRookHint(chessBoard, chessPiece, myColor); + case knight: + return getKnightHint(chessBoard, chessPiece, myColor); + case bishop: + return getBishopHint(chessBoard, chessPiece, myColor); + case queen: + return getQueenHint(chessBoard, chessPiece, myColor); + case king: + return getKingHint(chessBoard, chessPiece, myColor); + } + + return []; +} \ No newline at end of file