diff --git a/client/.env b/client/.env new file mode 100644 index 0000000..828dd3e --- /dev/null +++ b/client/.env @@ -0,0 +1 @@ +VITE_SERVER_URL="ws://localhost:5000" \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index ea534c0..67c312f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1891,6 +1891,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } diff --git a/client/src/App.tsx b/client/src/App.tsx index 0ad638d..2d65621 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import { BrowserRouter, Route, Routes } from "react-router"; -import ConnectionProvider from "./ConnectionProvider"; +import { ConnectionProvider } from "@/features/connection"; import { GameScreen, MenuScreen } from "@/pages"; function App() { diff --git a/client/src/ConnectionProvider/Context.ts b/client/src/ConnectionProvider/Context.ts deleted file mode 100644 index 9748e9f..0000000 --- a/client/src/ConnectionProvider/Context.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createContext } from "react"; -import { connect } from "socket.io-client"; -import type { ConnectionContext, CustomSocket } from "types/Socket"; - -export const socket: CustomSocket = connect("ws://localhost:5000", { - autoConnect: false, -}); - -export const ConnContext = createContext({ - socket, -}); diff --git a/client/src/ConnectionProvider/index.tsx b/client/src/ConnectionProvider/index.tsx deleted file mode 100644 index d6d7ad1..0000000 --- a/client/src/ConnectionProvider/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from "react"; -import { useParams } from "react-router"; -import { ConnContext, socket } from "./Context"; - -function ConnectionProvider(props: { children: React.ReactNode }) { - // Get the code from the URL - const { code } = useParams(); - - // Create a socket connection - useEffect(() => { - if (socket.connected) return; - - socket.io.opts.query = { code }; - - socket.on("connect", () => console.log("Connected to server")); - socket.on("connect_error", (err) => console.error(err)); - socket.on("disconnect", () => console.log("Disconnected from server")); - socket.connect(); - - return () => { - socket.disconnect(); - }; - }, [code]); - - return ( - - {props.children} - - ); -} - -export default ConnectionProvider; diff --git a/client/src/components/Button/index.tsx b/client/src/components/Button/index.tsx index 308d3ce..7f7e7e9 100644 --- a/client/src/components/Button/index.tsx +++ b/client/src/components/Button/index.tsx @@ -25,6 +25,8 @@ function Button({ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black", customClassName )} + disabled={disabled} + aria-disabled={disabled} type="button" disabled={disabled} aria-disabled={disabled} diff --git a/client/src/components/Input/index.tsx b/client/src/components/Input/index.tsx index 0ae80e2..da81a08 100644 --- a/client/src/components/Input/index.tsx +++ b/client/src/components/Input/index.tsx @@ -1,14 +1,17 @@ -import { forwardRef, type ComponentPropsWithoutRef } from "react"; + +import clsx from "clsx"; +import React, { forwardRef, type ComponentPropsWithoutRef } from "react"; type InputProps = ComponentPropsWithoutRef<"input">; -const Input = forwardRef(({ ...props }, ref) => ( +const Input = forwardRef(({ className, ...props }, ref) => ( )); diff --git a/client/src/components/Modal/index.tsx b/client/src/components/Modal/index.tsx new file mode 100644 index 0000000..1bf610d --- /dev/null +++ b/client/src/components/Modal/index.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useRef } from "react"; + +interface ModalProps { + onClose?: () => void; + children: React.ReactNode; + showCloseIcon?: boolean; + canBeDismissed?: boolean; + isOpened: boolean; +} + +function Modal({ onClose, showCloseIcon = true, canBeDismissed = true, children, isOpened }: ModalProps) { + const dialogRef = useRef(null); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape" && !canBeDismissed) e.preventDefault(); + }; + + const handleModalClose = () => { + onClose?.(); + dialogRef.current?.close(); + }; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget && canBeDismissed) { + handleModalClose(); + } + }; + + useEffect(() => { + if (isOpened) { + dialogRef.current?.showModal(); + } else { + dialogRef.current?.close(); + } + }, [isOpened]); + + return ( + +
+ {showCloseIcon && ( + + )} + {children} +
+
+ ); +} + +export default Modal; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index 088ea42..5c4f658 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -1,4 +1,5 @@ import Button from "./Button"; import Input from "./Input"; +import Modal from "./Modal"; -export { Button, Input }; +export { Button, Input, Modal }; diff --git a/client/src/constants/index.ts b/client/src/constants/index.ts new file mode 100644 index 0000000..d0d9fa1 --- /dev/null +++ b/client/src/constants/index.ts @@ -0,0 +1,5 @@ +import { socket } from "./socket"; + +const PLAYER_ID_KEY_NAME = "player_id"; + +export { socket, PLAYER_ID_KEY_NAME }; diff --git a/client/src/constants/socket.ts b/client/src/constants/socket.ts new file mode 100644 index 0000000..b7e890b --- /dev/null +++ b/client/src/constants/socket.ts @@ -0,0 +1,6 @@ +import type { CustomSocket } from "@/types/Socket"; +import { io } from "socket.io-client"; + +export const socket: CustomSocket = io(import.meta.env.VITE_SERVER_URL, { + autoConnect: false, +}); diff --git a/client/src/features/connection/ConnectionContext.ts b/client/src/features/connection/ConnectionContext.ts new file mode 100644 index 0000000..5983c62 --- /dev/null +++ b/client/src/features/connection/ConnectionContext.ts @@ -0,0 +1,10 @@ +import { createContext } from "react"; +import type { ConnectionContext } from "@/types/Socket"; +import { socket } from "@/constants"; +import { connect, disconnect } from "./utils"; + +export const ConnContext = createContext({ + socket, + connect, + disconnect, +}); diff --git a/client/src/features/connection/ConnectionProvider.tsx b/client/src/features/connection/ConnectionProvider.tsx new file mode 100644 index 0000000..3427b4e --- /dev/null +++ b/client/src/features/connection/ConnectionProvider.tsx @@ -0,0 +1,18 @@ +import { useMemo } from "react"; +import { useParams } from "react-router"; +import { ConnContext } from "./ConnectionContext"; +import { socket } from "@/constants"; +import { connect, disconnect } from "./utils"; +import useConnection from "./useConnection"; + +function ConnectionProvider(props: { children: React.ReactNode }) { + // Get the code from the URL + const { code } = useParams(); + useConnection(code); + + const contextValue = useMemo(() => ({ socket, connect, disconnect }), []); + + return {props.children}; +} + +export default ConnectionProvider; diff --git a/client/src/features/connection/index.ts b/client/src/features/connection/index.ts new file mode 100644 index 0000000..75e6fa1 --- /dev/null +++ b/client/src/features/connection/index.ts @@ -0,0 +1,4 @@ +import ConnectionProvider from "./ConnectionProvider"; +import { ConnContext } from "./ConnectionContext"; + +export { ConnContext, ConnectionProvider }; diff --git a/client/src/features/connection/useConnection.ts b/client/src/features/connection/useConnection.ts new file mode 100644 index 0000000..433ab3b --- /dev/null +++ b/client/src/features/connection/useConnection.ts @@ -0,0 +1,26 @@ +import { PLAYER_ID_KEY_NAME, socket } from "@/constants"; +import { useEffect } from "react"; +import { connect, disconnect } from "./utils"; + +function useConnection(code: string | undefined) { + useEffect(() => { + if (code === undefined) return; + + socket.on("connect", () => console.log("Connected to server")); + socket.on("connect_error", (err) => console.error(err)); + socket.on("disconnect", () => console.log("Disconnected from server")); + + socket.on("conn_info_data", ({ playerId }) => { + localStorage.setItem(PLAYER_ID_KEY_NAME, playerId); + }); + + connect(code); + + return () => { + disconnect(); + //TODO: remove registered listeners + }; + }, [code]); +} + +export default useConnection; diff --git a/client/src/features/connection/utils.ts b/client/src/features/connection/utils.ts new file mode 100644 index 0000000..d3f1be4 --- /dev/null +++ b/client/src/features/connection/utils.ts @@ -0,0 +1,20 @@ +import { PLAYER_ID_KEY_NAME, socket } from "@/constants"; + +const connect = (roomCode: string) => { + if (socket.connected) return; + + const playerId = localStorage.getItem(PLAYER_ID_KEY_NAME); + + if (playerId) { + socket.auth = { playerId }; + } + + socket.io.opts.query = { roomCode }; + socket.connect(); +}; + +const disconnect = () => { + socket.disconnect(); +}; + +export { connect, disconnect }; diff --git a/client/src/pages/Game/index.tsx b/client/src/pages/Game/index.tsx index 0ac700c..b4ef3f1 100644 --- a/client/src/pages/Game/index.tsx +++ b/client/src/pages/Game/index.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react"; -import { ConnContext } from "../../ConnectionProvider/Context"; +import { ConnContext } from "@/features/connection"; import { Phases } from "@global/Game"; import Lobby from "@/pages/Lobby"; @@ -9,7 +9,9 @@ function Game() { const [phase, setPhase] = useState(Phases.GAME_END); useEffect(() => { - socket.on("phaseChange", (data: Phases) => setPhase(data)); + socket.on("phase_updated", (phase) => { + setPhase(phase); + }); }, [socket]); // Render certain components based on the game phase diff --git a/client/src/pages/Lobby/index.tsx b/client/src/pages/Lobby/index.tsx index 93465c8..ec97e10 100644 --- a/client/src/pages/Lobby/index.tsx +++ b/client/src/pages/Lobby/index.tsx @@ -4,6 +4,8 @@ import Character from "./Character"; import Seat from "./Seats"; import Waiting from "./Waiting"; import useKeyDown from "@/hooks/useKeyDown"; +import { Button, Input, Modal } from "@/components"; +import { socket } from "@/constants"; enum Panels { Character = "Character", @@ -53,35 +55,56 @@ function Lobby() { }); }; + const [playerName, setPlayerName] = useState(""); + const [isModalOpened, setIsModalOpened] = useState(true); + const playerNameLength = playerName?.trim().length; + + const handleNameConfirmation = () => { + if (playerNameLength > 1) { + socket.emit("send_player_name", playerName); + setIsModalOpened(false); + } + }; + return ( -
-
-

MafiAKAI

+ <> + +

Enter your name

+

This will help other players bind your name with your character.

+ setPlayerName(e.target.value)} className="my-2" /> + +
-
Options
-
+
+
+

MafiAKAI

+
Options
+
-
- {panelsValues.map((panelName, i) => ( - - ))} -
+
+ {panelsValues.map((panelName, i) => ( + + ))} +
-
- {panelsValues[panelId] === Panels.Seat && ( - - )} - {panelsValues[panelId] === Panels.Character ? : <>} - {panelsValues[panelId] === Panels.Waiting ? : <>} - {panelsValues[panelId] === Panels.Character && } +
+ {panelsValues[panelId] === Panels.Seat && ( + + )} + {panelsValues[panelId] === Panels.Character ? : <>} + {panelsValues[panelId] === Panels.Waiting ? : <>} + {panelsValues[panelId] === Panels.Character && } +
-
+ ); } diff --git a/client/types/Socket.ts b/client/src/types/Socket.ts similarity index 55% rename from client/types/Socket.ts rename to client/src/types/Socket.ts index 5afd263..97e3be8 100644 --- a/client/types/Socket.ts +++ b/client/src/types/Socket.ts @@ -1,11 +1,10 @@ -import { - type Client2ServerEvents, - type Server2ClientEvents, -} from "@global/Sockets"; +import { type Client2ServerEvents, type Server2ClientEvents } from "@global/Sockets"; import { Socket } from "socket.io-client"; export type CustomSocket = Socket; export interface ConnectionContext { socket: CustomSocket; + connect: (roomCode: string) => void; + disconnect: () => void; } diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts index 11f02fe..d8fcf94 100644 --- a/client/src/vite-env.d.ts +++ b/client/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_SERVER_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 9b314bb..be80715 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -28,5 +28,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src", "types"] + "include": ["src"] } diff --git a/package.json b/package.json index 7b8e0b8..a580661 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "scripts": { "client:dev": "npm --prefix ./client run dev", "server:dev": "npm --prefix ./server run dev", - "all:dev": "npx concurrently \"npm run client:dev\" \"npm run server:dev\"", + "all:dev": "concurrently \"npm run client:dev\" \"npm run server:dev\"", "format:front": "prettier --write \"./client/src/**/*.{js,jsx,ts,tsx}\"", - "format:back": "prettier --write \"./server/src/**/*.{js,jsx,ts,tsx}\"" + "format:back": "prettier --write \"./server/src/**/*.{js,jsx,ts,tsx}\"", + "instDeps": "concurrently \"npm i\" \"cd server && npm i\" \"cd client && npm i\"" }, "repository": { "type": "git", diff --git a/server/package.json b/server/package.json index 6ce321c..ba19074 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "index.ts", "scripts": { - "dev": "tsx watch index.ts" + "dev": "tsx watch src/index.ts" }, "author": "", "license": "ISC", diff --git a/server/src/Player.ts b/server/src/Player.ts deleted file mode 100644 index 554bf74..0000000 --- a/server/src/Player.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Roles } from "@global/Roles"; - -export class Player { - constructor( - public name: string, - public id: number, - public role: Roles - ) {} -} diff --git a/server/src/Room.ts b/server/src/Room.ts deleted file mode 100644 index a4ae197..0000000 --- a/server/src/Room.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Phases } from "@global/Game"; -import { Player } from "./Player"; - -export class Room { - code: string; - players: Array = new Array(); - phase: Phases = Phases.LOBBY; - - constructor(code: string) { - this.code = code; - } - - addPlayer(player: Player) { - this.players.push(player); - } - - addPlayerAt(playerId: number, player: Player) { - playerId = Math.max(playerId, this.players.length); - playerId = Math.min(playerId, 0); - - this.players.splice(playerId, 0, player); - } - - removePlayer(playerId: number) { - delete this.players[playerId]; - } -} diff --git a/server/config.ts b/server/src/constants/config.ts similarity index 100% rename from server/config.ts rename to server/src/constants/config.ts diff --git a/server/src/constants/index.ts b/server/src/constants/index.ts new file mode 100644 index 0000000..8f336c3 --- /dev/null +++ b/server/src/constants/index.ts @@ -0,0 +1,4 @@ +import { manager } from "./manager"; +import config from "./config"; + +export { manager, config }; diff --git a/server/src/constants/manager.ts b/server/src/constants/manager.ts new file mode 100644 index 0000000..debc02a --- /dev/null +++ b/server/src/constants/manager.ts @@ -0,0 +1,3 @@ +import { RoomManager } from "@/models"; + +export const manager = new RoomManager(); diff --git a/server/index.ts b/server/src/index.ts similarity index 54% rename from server/index.ts rename to server/src/index.ts index 3045211..d0cdd50 100644 --- a/server/index.ts +++ b/server/src/index.ts @@ -2,12 +2,10 @@ import express from "express"; import http from "http"; import { Server } from "socket.io"; -import { MAServer } from "@local/Sockets"; -import config from "./config"; - -import socketAuth from "@/socketAuth"; -import socketRoutes from "@/socketRoutes"; -import webRoutes from "@/webRoutes"; +import { MAServer } from "@/types"; +import { config } from "@/constants"; +import { socketAuth } from "@/middlewares"; +import { socketRoutes, webRouter } from "@/routes"; const app = express(); // Create an Express application @@ -24,10 +22,10 @@ const socketsServer: MAServer = new Server(httpServer, { // Configure servers socketsServer.use(socketAuth).on("connection", socketRoutes); // Configure the WebSocket server -app.use("/", webRoutes); // Configure the HTTP server +app.use("/", webRouter); // Configure the HTTP server // Start servers -httpServer.listen(config.PORT); // See https://socket.io/docs/v4/server-initialization/#with-express -console.log( - `Server is running on${"\x1b[34m"} http://${config.HOST}:${config.PORT}${"\x1b[0m"}` -); +httpServer.listen(config.PORT, () => { + console.log(`Server is running on${"\x1b[34m"} http://${config.HOST}:${config.PORT}${"\x1b[0m"}`); +}); +// See https://socket.io/docs/v4/server-initialization/#with-express diff --git a/server/src/middlewares/index.ts b/server/src/middlewares/index.ts new file mode 100644 index 0000000..c8bb283 --- /dev/null +++ b/server/src/middlewares/index.ts @@ -0,0 +1,3 @@ +import socketAuth from "./socketAuth"; + +export { socketAuth }; diff --git a/server/src/middlewares/socketAuth.ts b/server/src/middlewares/socketAuth.ts new file mode 100644 index 0000000..a384d1d --- /dev/null +++ b/server/src/middlewares/socketAuth.ts @@ -0,0 +1,41 @@ +import { ExtendedError } from "socket.io"; +import { MASocket } from "@/types"; +import { manager } from "@/constants"; +import { NON_STRICT_PHASES } from "@global/Game"; + +/* + Validate connection and Join player to room + To establish a connection, the client must provide: + - code - the room code + - id - old player id or undefined +*/ +export default function socketAuth(socket: MASocket, next: (err?: ExtendedError) => void) { + // Code + const code = socket.handshake.query.roomCode; + if (code === undefined) return next(new Error("Invalud code provided")); + if (typeof code !== "string") return next(new Error("Invalid code provided")); + + // Room + const room = manager.getRoom(code); + if (room === undefined) return next(new Error(`Room ${code} does not exist`)); + + const playerId = socket.handshake.auth.playerId; + + // Join as old player + if (room.hasPlayer(playerId)) { + socket.data = { playerId, roomCode: code }; + return next(); + } + + // Join as new player + if (NON_STRICT_PHASES.includes(room.phase)) { + const newPlayerId = manager.generatePlayerId(); + room.addPlayer(newPlayerId); + + socket.data = { playerId: newPlayerId, roomCode: code }; + return next(); + } + + // Can't join + return next(new Error(`Room ${code} does not accept new players currently`)); +} diff --git a/server/src/models/Player.ts b/server/src/models/Player.ts new file mode 100644 index 0000000..707695d --- /dev/null +++ b/server/src/models/Player.ts @@ -0,0 +1,14 @@ +import { Persona } from "@global/Persona"; +import { Roles } from "@global/Roles"; + +export class Player { + online: boolean = true; + persona: Persona = {}; + role: Roles | null = null; + seat: number | null = null; + name: string | null = null; // Real name of the player + + constructor( + public id: string // Unique identifier for the player + ) {} +} diff --git a/server/src/models/Room.ts b/server/src/models/Room.ts new file mode 100644 index 0000000..a515a67 --- /dev/null +++ b/server/src/models/Room.ts @@ -0,0 +1,50 @@ +import { NON_STRICT_PHASES, Phases } from "@global/Game"; +import { Player } from "./Player"; + +export class Room { + code: string; + private players = new Map(); + phase: Phases = Phases.LOBBY; + + constructor(code: string) { + this.code = code; + } + + addPlayer(playerId: string) { + this.players.set(playerId, new Player(playerId)); + } + + disconnectPlayer(playerId: string) { + if (NON_STRICT_PHASES.includes(this.phase)) { + this.players.delete(playerId); + } else { + this.players.get(playerId)!.online = false; + } + } + + hasPlayer(playerId: string) { + return this.players.has(playerId); + } + + getPlayer(playerId: string): Player | undefined { + return this.players.get(playerId); + } + + getPlayers() { + return Array.from(this.players.values()); + } + + setPlayerSeat(playerId: string, seat: number) { + for (const player of this.getPlayers()) { + if (player.seat && player.seat >= seat) player.seat++; + } + + this.players.get(playerId)!.seat = seat; + } + + getPlayersBySeat() { + return this.getPlayers() + .filter((player) => player.seat) + .sort((a, b) => a.seat! - b.seat!); + } +} diff --git a/server/src/RoomManager.ts b/server/src/models/RoomManager.ts similarity index 77% rename from server/src/RoomManager.ts rename to server/src/models/RoomManager.ts index a17d811..b7e81d4 100644 --- a/server/src/RoomManager.ts +++ b/server/src/models/RoomManager.ts @@ -1,8 +1,8 @@ import { Room } from "./Room"; +import crypto from "node:crypto"; -class RoomManager { +export class RoomManager { rooms = new Map([["000000", new Room("000000")]]); - private playerIdCounter = 1; // unique 5 digits room code generateCode(): string { @@ -13,8 +13,8 @@ class RoomManager { return code; } - generatePlayerId(): number { - return this.playerIdCounter++; + generatePlayerId(): string { + return crypto.randomUUID(); } create(): string { @@ -28,5 +28,3 @@ class RoomManager { return this.rooms.get(code); } } - -export const manager = new RoomManager(); diff --git a/server/src/models/index.ts b/server/src/models/index.ts new file mode 100644 index 0000000..8a9a007 --- /dev/null +++ b/server/src/models/index.ts @@ -0,0 +1,5 @@ +import { Player } from "./Player"; +import { Room } from "./Room"; +import { RoomManager } from "./RoomManager"; + +export { Player, Room, RoomManager }; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts new file mode 100644 index 0000000..b82bd20 --- /dev/null +++ b/server/src/routes/index.ts @@ -0,0 +1,4 @@ +import socketRoutes from "./socketRoutes"; +import webRouter from "./webRoutes"; + +export { socketRoutes, webRouter }; diff --git a/server/src/routes/socketRoutes.ts b/server/src/routes/socketRoutes.ts new file mode 100644 index 0000000..e1a239b --- /dev/null +++ b/server/src/routes/socketRoutes.ts @@ -0,0 +1,23 @@ +import { MASocket } from "@/types"; +import { manager } from "@/constants"; + +// Here we set up the socket events for the client +export default function socketRoutes(socket: MASocket) { + //TODO: Should player be assigned to socket room too? + const room = manager.getRoom(socket.data.roomCode)!; + const playerId = socket.data.playerId; + + socket.emit("conn_info_data", { + playerId: socket.data.playerId, + }); + + socket.emit("phase_updated", room.phase); + + socket.on("disconnect", () => { + room?.disconnectPlayer(playerId); + }); + + socket.on("send_player_name", (playerName) => { + room.getPlayer(playerId)!.name = playerName; + }); +} diff --git a/server/src/webRoutes.ts b/server/src/routes/webRoutes.ts similarity index 80% rename from server/src/webRoutes.ts rename to server/src/routes/webRoutes.ts index 78cafa9..1d9ea99 100644 --- a/server/src/webRoutes.ts +++ b/server/src/routes/webRoutes.ts @@ -1,5 +1,5 @@ import express from "express"; -import { manager } from "./RoomManager"; +import { manager } from "@/constants"; const webRouter = express.Router(); @@ -20,7 +20,7 @@ webRouter.get("/room/:code", (req, res) => { return; } - res.json({ roomCode: room.code, playersInRoom: room.players.length }); + res.json({ roomCode: room.code, playersInRoom: room.getPlayers().length }); }); export default webRouter; diff --git a/server/src/socketAuth.ts b/server/src/socketAuth.ts deleted file mode 100644 index 3d03349..0000000 --- a/server/src/socketAuth.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ExtendedError } from "socket.io"; -import { MASocket } from "@local/Sockets"; -import { manager } from "./RoomManager"; -import { Phases } from "@global/Game"; - -// Connection validation -export default function socketAuth( - socket: MASocket, - next: (err?: ExtendedError) => void -) { - // check if code is valid - const code = socket.handshake.query.code; - - if (code === undefined) return next(new Error("No code provided")); - if (typeof code !== "string") return next(new Error("Invalid code provided")); - - // check if room exists - const room = manager.getRoom(code); - if (room === undefined) return next(new Error(`Room ${code} does not exist`)); - - // check if player can join - if ( - ![ - Phases.LOBBY, - Phases.POSITION_SELECTION, - Phases.CHARACTER_SELECTION, - Phases.ROLE_ASSIGNMENT, - Phases.WELCOME, - ].includes(room.phase) - ) - return next( - new Error(`ERROR: Room ${code} does not accept new players currently`) - ); - - return next(); -} diff --git a/server/src/socketRoutes.ts b/server/src/socketRoutes.ts deleted file mode 100644 index ad63a0d..0000000 --- a/server/src/socketRoutes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MASocket } from "@local/Sockets"; -import { manager } from "./RoomManager"; -import { Player } from "./Player"; -import { Roles } from "@global/Roles"; -import { Phases } from "@global/Game"; - -export default function socketRoutes(socket: MASocket) { - // these variables are valid because of auth middleware - const code = socket.handshake.query.code as string; - const room = manager.getRoom(code)!; - - console.log(`${socket.id} joinRoom ${code}`); - socket.join(code); - - socket.emit("phaseChange", room.phase); - - const playerid = manager.generatePlayerId(); - - room.addPlayer( - new Player(`player-${playerid}`, playerid, Roles.REGULAR_CITIZEN) - ); - - socket.on("vote", (data) => { - socket.emit("info", `You voted for ${data}`); - }); -} diff --git a/server/types/Sockets.ts b/server/src/types/index.ts similarity index 100% rename from server/types/Sockets.ts rename to server/src/types/index.ts diff --git a/types/Game.ts b/types/Game.ts index 77280a6..7e72475 100644 --- a/types/Game.ts +++ b/types/Game.ts @@ -1,96 +1,105 @@ export enum Phases { - /** - * Faza LOBBY: Gracze dołączają do gry. Gra czeka, aż wszyscy gracze zgłoszą gotowość. - */ - LOBBY = "LOBBY", - - /** - * Faza POSITION_SELECTION: Gracze wybierają swoje pozycje (np. miejsca przy stole). - * Jeśli wszyscy gracze są gotowi, gra przechodzi do CHARACTER_SELECTION. - */ - POSITION_SELECTION = "POSITION_SELECTION", - - /** - * Faza CHARACTER_SELECTION: Gracze wybierają swoje jawne postacie. - * Jeśli wszyscy gracze są gotowi, gra przechodzi do ROLE_ASSIGNMENT. - */ - CHARACTER_SELECTION = "CHARACTER_SELECTION", - - /** - * Faza ROLE_ASSIGNMENT: Gracze otrzymują swoje tajne role (np. obywatel, mafia). - * Po zakończeniu losowania, gra automatycznie przechodzi do WELCOME. - */ - ROLE_ASSIGNMENT = "ROLE_ASSIGNMENT", - - /** - * Faza WELCOME: Krótkie wprowadzenie do gry. Gracze mogą zapoznać się z zasadami. - * Po określonym czasie gra automatycznie przechodzi do ROUND_START. - */ - WELCOME = "WELCOME", - - /** - * Faza ROUND_START: Oficjalne rozpoczęcie rundy. Trwa przez krótki czas, zanim gra przejdzie do DAY. - */ - ROUND_START = "ROUND_START", - - /** - * Faza DAY: Początek dnia w grze. Gracze mogą rozmawiać i analizować wydarzenia. - * Po określonym czasie gra przechodzi do DEBATE. - */ - DAY = "DAY", - - /** - * Faza DEBATE: Gracze prowadzą debatę na temat swoich podejrzeń. - * Po określonym czasie gra przechodzi do VOTING. - */ - DEBATE = "DEBATE", - - /** - * Faza VOTING: Gracze głosują na osobę, która ich zdaniem powinna zostać wyeliminowana. - * Jeśli dwie lub więcej osób otrzyma taką samą największą liczbę głosów, gra przechodzi do VOTING_OVERTIME. - * W przeciwnym razie, gra przechodzi do NIGHT. - */ - VOTING = "VOTING", - - /** - * Faza VOTING_OVERTIME: Dogrywka głosowania, gdy dwie lub więcej osób otrzymało największą liczbę głosów. - * Po rozstrzygnięciu gra przechodzi do NIGHT. - */ - VOTING_OVERTIME = "VOTING_OVERTIME", - - /** - * Faza NIGHT: Gracze wykonują swoje nocne akcje. - * Gra automatycznie przechodzi do BODYGUARD_DEFENSE. - */ - NIGHT = "NIGHT", - - /** - * Faza BODYGUARD_DEFENSE: Ochroniarz wybiera, kogo chce obronić przed atakiem mafii. - * Jeśli ochroniarz wykona swoją akcję, gra przechodzi do DETECTIVE_CHECK. - */ - BODYGUARD_DEFENSE = "BODYGUARD_DEFENSE", - - /** - * Faza DETECTIVE_CHECK: Detektyw wybiera osobę, której tożsamość chce sprawdzić (obywatel/mafia). - * Po wykonaniu akcji przez detektywa gra przechodzi do MAFIA_VOTING. - */ - DETECTIVE_CHECK = "DETECTIVE_CHECK", - - /** - * Faza MAFIA_VOTING: Mafia wspólnie wybiera osobę, którą chce wyeliminować. - * Jeśli mafia dokona wyboru lub upłynie określony czas, gra przechodzi do ROUND_END. - */ - MAFIA_VOTING = "MAFIA_VOTING", - - /** - * Faza ROUND_END: Podsumowanie rundy. Gra pokazuje wyniki nocnych akcji (np. kto został zabity). - * Jeśli wszyscy mafiozi zostali wyeliminowani lub liczba mafiozów >= liczba obywateli, gra przechodzi do GAME_END. - * W przeciwnym razie gra wraca do DAY. - */ - ROUND_END = "ROUND_END", - - /** - * Faza GAME_END: Zakończenie gry. Gra pokazuje wyniki i zwycięzców (obywatele lub mafia). - */ - GAME_END = "GAME_END" + /** + * Faza LOBBY: Gracze dołączają do gry. Gra czeka, aż wszyscy gracze zgłoszą gotowość. + */ + LOBBY = "LOBBY", + + /** + * Faza POSITION_SELECTION: Gracze wybierają swoje pozycje (np. miejsca przy stole). + * Jeśli wszyscy gracze są gotowi, gra przechodzi do CHARACTER_SELECTION. + */ + POSITION_SELECTION = "POSITION_SELECTION", + + /** + * Faza CHARACTER_SELECTION: Gracze wybierają swoje jawne postacie. + * Jeśli wszyscy gracze są gotowi, gra przechodzi do ROLE_ASSIGNMENT. + */ + CHARACTER_SELECTION = "CHARACTER_SELECTION", + + /** + * Faza ROLE_ASSIGNMENT: Gracze otrzymują swoje tajne role (np. obywatel, mafia). + * Po zakończeniu losowania, gra automatycznie przechodzi do WELCOME. + */ + ROLE_ASSIGNMENT = "ROLE_ASSIGNMENT", + + /** + * Faza WELCOME: Krótkie wprowadzenie do gry. Gracze mogą zapoznać się z zasadami. + * Po określonym czasie gra automatycznie przechodzi do ROUND_START. + */ + WELCOME = "WELCOME", + + /** + * Faza ROUND_START: Oficjalne rozpoczęcie rundy. Trwa przez krótki czas, zanim gra przejdzie do DAY. + */ + ROUND_START = "ROUND_START", + + /** + * Faza DAY: Początek dnia w grze. Gracze mogą rozmawiać i analizować wydarzenia. + * Po określonym czasie gra przechodzi do DEBATE. + */ + DAY = "DAY", + + /** + * Faza DEBATE: Gracze prowadzą debatę na temat swoich podejrzeń. + * Po określonym czasie gra przechodzi do VOTING. + */ + DEBATE = "DEBATE", + + /** + * Faza VOTING: Gracze głosują na osobę, która ich zdaniem powinna zostać wyeliminowana. + * Jeśli dwie lub więcej osób otrzyma taką samą największą liczbę głosów, gra przechodzi do VOTING_OVERTIME. + * W przeciwnym razie, gra przechodzi do NIGHT. + */ + VOTING = "VOTING", + + /** + * Faza VOTING_OVERTIME: Dogrywka głosowania, gdy dwie lub więcej osób otrzymało największą liczbę głosów. + * Po rozstrzygnięciu gra przechodzi do NIGHT. + */ + VOTING_OVERTIME = "VOTING_OVERTIME", + + /** + * Faza NIGHT: Gracze wykonują swoje nocne akcje. + * Gra automatycznie przechodzi do BODYGUARD_DEFENSE. + */ + NIGHT = "NIGHT", + + /** + * Faza BODYGUARD_DEFENSE: Ochroniarz wybiera, kogo chce obronić przed atakiem mafii. + * Jeśli ochroniarz wykona swoją akcję, gra przechodzi do DETECTIVE_CHECK. + */ + BODYGUARD_DEFENSE = "BODYGUARD_DEFENSE", + + /** + * Faza DETECTIVE_CHECK: Detektyw wybiera osobę, której tożsamość chce sprawdzić (obywatel/mafia). + * Po wykonaniu akcji przez detektywa gra przechodzi do MAFIA_VOTING. + */ + DETECTIVE_CHECK = "DETECTIVE_CHECK", + + /** + * Faza MAFIA_VOTING: Mafia wspólnie wybiera osobę, którą chce wyeliminować. + * Jeśli mafia dokona wyboru lub upłynie określony czas, gra przechodzi do ROUND_END. + */ + MAFIA_VOTING = "MAFIA_VOTING", + + /** + * Faza ROUND_END: Podsumowanie rundy. Gra pokazuje wyniki nocnych akcji (np. kto został zabity). + * Jeśli wszyscy mafiozi zostali wyeliminowani lub liczba mafiozów >= liczba obywateli, gra przechodzi do GAME_END. + * W przeciwnym razie gra wraca do DAY. + */ + ROUND_END = "ROUND_END", + + /** + * Faza GAME_END: Zakończenie gry. Gra pokazuje wyniki i zwycięzców (obywatele lub mafia). + */ + GAME_END = "GAME_END", } + +/** + * Fazy gry, do których mogą dołączyć nowi gracze. + */ +export const NON_STRICT_PHASES: ReadonlyArray = [ + Phases.LOBBY, + Phases.POSITION_SELECTION, + Phases.CHARACTER_SELECTION, +]; diff --git a/types/Persona.ts b/types/Persona.ts new file mode 100644 index 0000000..1c82b99 --- /dev/null +++ b/types/Persona.ts @@ -0,0 +1 @@ +export interface Persona {} diff --git a/types/Roles.ts b/types/Roles.ts index dfcb80f..fe58bd2 100644 --- a/types/Roles.ts +++ b/types/Roles.ts @@ -2,34 +2,34 @@ * Role są tajnymi funkcjami pełnionymi przez wybranych losowo graczy. * Przynależność danego gracza do wybranej roli nie jest powszechnie znana, poza wypisanymi niżej wyjątkami. */ -export enum Roles{ - /** - * Gracze pełniący rolę mafii mogą głosować w trakcie fazy MAFIA_VOTING za eliminacją wybranego obywatela. - * Przynależność do mafii jest tajna poza jej kręgiem członków. - * Rola jest pełniona grupowo. - */ - MAFIOSO = "MAFIOSO", +export enum Roles { + /** + * Gracze pełniący rolę mafii mogą głosować w trakcie fazy MAFIA_VOTING za eliminacją wybranego obywatela. + * Przynależność do mafii jest tajna poza jej kręgiem członków. + * Rola jest pełniona grupowo. + */ + MAFIOSO = "MAFIOSO", - /** - * Detektyw posiada możliwość sprawdzenia tożsamości danego gracza (jego roli) w fazie DETECTIVE_CHECK. - * Nie posiada on jednak ważniejszych zdolności egzekucyjnych poza publiczną debatą (DEBATE) i publicznym głosowaniem (VOTING). - * Z oczywistych względów narażony na ataki mafii w przypadku wyjawienia tożsamości. - * Rola jest pełniona indywidualnie. - */ - DETECTIVE = "DETECTIVE", + /** + * Detektyw posiada możliwość sprawdzenia tożsamości danego gracza (jego roli) w fazie DETECTIVE_CHECK. + * Nie posiada on jednak ważniejszych zdolności egzekucyjnych poza publiczną debatą (DEBATE) i publicznym głosowaniem (VOTING). + * Z oczywistych względów narażony na ataki mafii w przypadku wyjawienia tożsamości. + * Rola jest pełniona indywidualnie. + */ + DETECTIVE = "DETECTIVE", - /** - * Ochroniarz w trakcie fazy BODYGUARD_DEFENSE wybiera jedną osobę, którą chroni przed atakiem mafii. - * Może on także wybrać siebie. - * Rola jest pełniona indywidualnie. - */ - BODYGUARD = "BODYGUARD", + /** + * Ochroniarz w trakcie fazy BODYGUARD_DEFENSE wybiera jedną osobę, którą chroni przed atakiem mafii. + * Może on także wybrać siebie. + * Rola jest pełniona indywidualnie. + */ + BODYGUARD = "BODYGUARD", - /** - * Każdy z graczy ma możliwość uczestniczenia w standardowych fazach dnia (DAY), debaty (DEBATE), głosowania (VOTING, VOTING_OVERTIME). - * - * Rola jest pełniona grupowo i jest domyślnym wariantem, - * której funkcje spełniają także gracze przypisani do wyżej wymienionych ról. - */ - REGULAR_CITIZEN = "REGULAR_CITIZEN" -}; \ No newline at end of file + /** + * Każdy z graczy ma możliwość uczestniczenia w standardowych fazach dnia (DAY), debaty (DEBATE), głosowania (VOTING, VOTING_OVERTIME). + * + * Rola jest pełniona grupowo i jest domyślnym wariantem, + * której funkcje spełniają także gracze przypisani do wyżej wymienionych ról. + */ + REGULAR_CITIZEN = "REGULAR_CITIZEN", +} diff --git a/types/Sockets.ts b/types/Sockets.ts index 302502f..fe9ecbe 100644 --- a/types/Sockets.ts +++ b/types/Sockets.ts @@ -1,16 +1,18 @@ import type { Phases } from "./Game"; export interface Server2ClientEvents { - rooms: (rooms: Array) => void; - info: (data: string) => void; - phaseChange: (phase: Phases) => void; + rooms_data: (rooms: Array) => void; + conn_info_data: (data: ConnectionInfoData) => void; + phase_updated: (phase: Phases) => void; } +export type ConnectionInfoData = { playerId: string }; export type ResponseHandler = (message: string) => void; export interface Client2ServerEvents { setPosition: (position: number, callback: ResponseHandler) => void; vote: (data: string) => void; + send_player_name: (playerName: string) => void; } export interface InterServerEvents { @@ -18,6 +20,6 @@ export interface InterServerEvents { } export interface SocketData { - name: string; - age: number; + playerId: string; + roomCode: string; }