aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--backend/src/app.ts5
-rw-r--r--backend/src/config/coop-session-store.ts73
-rw-r--r--backend/src/config/session-store.ts77
-rw-r--r--backend/src/model/actions/coop.ts43
-rw-r--r--backend/src/model/session.ts16
-rw-r--r--backend/src/model/websocket.ts7
-rw-r--r--backend/src/routes/session.route.ts6
-rw-r--r--backend/src/websocket/coop.ts142
-rw-r--r--backend/src/websocket/game.ts97
-rw-r--r--backend/src/websocket/websocket-router.ts18
-rw-r--r--frontend/src/views/HostView.vue57
-rw-r--r--frontend/src/views/JoinView.vue43
12 files changed, 354 insertions, 230 deletions
diff --git a/backend/src/app.ts b/backend/src/app.ts
index 41b8821..1f5df24 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -1,8 +1,7 @@
import express from 'express';
import router from './router.js';
import cors from 'cors';
-import createWebsocketServer from './websocket/game.js';
-import { WebSocketServer } from 'ws';
+import createWebsocketServer from './websocket/websocket-router.js';
const app = express();
@@ -19,4 +18,4 @@ const server = app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
-const websocketServer: WebSocketServer = createWebsocketServer(server); \ No newline at end of file
+createWebsocketServer(server); \ No newline at end of file
diff --git a/backend/src/config/coop-session-store.ts b/backend/src/config/coop-session-store.ts
new file mode 100644
index 0000000..09fef87
--- /dev/null
+++ b/backend/src/config/coop-session-store.ts
@@ -0,0 +1,73 @@
+import { CoopSession, SessionState } from "../model/session";
+import { CoopWebSocket } from "../model/websocket";
+
+const sessions: { [key: string]: CoopSession } = {};
+
+function makeid(length) {
+ let result = '';
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ const charactersLength = characters.length;
+ let counter = 0;
+ while (counter < length) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ counter += 1;
+ }
+ return result;
+}
+
+
+export const createNewSession = (host: CoopWebSocket): CoopSession => {
+ const id = makeid(10);
+ const session: CoopSession = {
+ id,
+ state: "waiting",
+ host: host,
+ clients: [],
+ };
+ sessions[id] = session;
+ return session;
+};
+
+export const setSessionState = (id: string, state: SessionState): void => {
+ if (!sessions[id]) {
+ return;
+ }
+
+ sessions[id].state = state;
+
+ if (state === "finished") {
+ cleanupSession(id);
+ }
+};
+
+export const setSessionHost = (id: string, client: CoopWebSocket): void => {
+ if (!sessions[id]) {
+ return;
+ }
+
+ sessions[id].host = client;
+};
+
+export const addSessionClient = (id: string, client: CoopWebSocket): void => {
+ if (!sessions[id]) {
+ return;
+ }
+
+ sessions[id]?.clients.push(client);
+};
+
+export const cleanupSession = (id: string): void => {
+ if (!sessions[id]) {
+ return;
+ }
+
+ sessions[id].host.close();
+
+ sessions[id].clients.forEach(client => client.close());
+
+ delete sessions[id];
+}
+
+export const getSession = (id: string): CoopSession => {
+ return sessions[id];
+}; \ No newline at end of file
diff --git a/backend/src/config/session-store.ts b/backend/src/config/session-store.ts
deleted file mode 100644
index 283031b..0000000
--- a/backend/src/config/session-store.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-export type Session = {
- id: string;
- state: string;
- host?: string;
- clients: string[];
-};
-
-const sessions: { [key: string]: Session } = {};
-
-function makeid(length) {
- let result = '';
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- const charactersLength = characters.length;
- let counter = 0;
- while (counter < length) {
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
- counter += 1;
- }
- return result;
-}
-
-
-export const createNewSession = (): Session => {
- const id = makeid(10);
- const session = {
- id,
- state: "waiting",
- host: undefined,
- clients: [],
- };
- sessions[id] = session;
- return session;
-};
-
-export const setSessionState = (id: string, state: string): void => {
- if (!sessions[id]) {
- return;
- }
-
- sessions[id].state = state;
-};
-
-export const setSessionHost = (id: string, clientId: string): void => {
- if (!sessions[id]) {
- return;
- }
-
- sessions[id].host = clientId;
-};
-
-export const addSessionClient = (id: string, clientId: string): void => {
- if (!sessions[id]) {
- return;
- }
-
- sessions[id]?.clients.push(clientId);
-};
-
-export const cleanupSession = (id: string): void => {
- if (!sessions[id]) {
- return;
- }
-
-// if (sessions[id].host) {
-// sessions[id].host!.close();
-// }
-//
-// sessions[id].clients.forEach((client) => {
-// client.close();
-// });
-
- delete sessions[id];
-}
-
-export const getSession = (id: string): Session => {
- return sessions[id];
-}; \ No newline at end of file
diff --git a/backend/src/model/actions/coop.ts b/backend/src/model/actions/coop.ts
new file mode 100644
index 0000000..e1dfed5
--- /dev/null
+++ b/backend/src/model/actions/coop.ts
@@ -0,0 +1,43 @@
+import { SessionState } from "../session";
+
+export type CoopSessionCreatedMessage = {
+ type: "create";
+ payload: {
+ sessionId: string;
+ };
+};
+
+export type ClientJoinedCoopSessionMessage = {
+ type: "join";
+ payload: {
+ numberOfClients: number;
+ };
+};
+
+export type CoopSessionStateChangeMessage = {
+ type: "state";
+ payload: {
+ state: SessionState;
+ };
+};
+
+export type CoopSessionMoveMessage = {
+ type: "move";
+ payload: {
+ move: string;
+ };
+};
+
+export type CoopSessionMoveTimeoutMessage = {
+ type: "timeout";
+ payload: {
+ timeoutUntil: number;
+ };
+};
+
+export type CoopSessionMessage =
+ | CoopSessionCreatedMessage
+ | ClientJoinedCoopSessionMessage
+ | CoopSessionStateChangeMessage
+ | CoopSessionMoveMessage
+ | CoopSessionMoveTimeoutMessage; \ No newline at end of file
diff --git a/backend/src/model/session.ts b/backend/src/model/session.ts
new file mode 100644
index 0000000..d2fdea3
--- /dev/null
+++ b/backend/src/model/session.ts
@@ -0,0 +1,16 @@
+import { CoopWebSocket } from "./websocket";
+
+export type CoopSession = {
+ id: string;
+ state: SessionState;
+ host: CoopWebSocket;
+ clients: CoopWebSocket[];
+};
+
+export type VersusSession = {
+ id: string;
+ state: SessionState;
+ clients: string[];
+};
+
+export type SessionState = 'waiting' | 'playing' | 'finished'; \ No newline at end of file
diff --git a/backend/src/model/websocket.ts b/backend/src/model/websocket.ts
new file mode 100644
index 0000000..bb2ae8d
--- /dev/null
+++ b/backend/src/model/websocket.ts
@@ -0,0 +1,7 @@
+import { WebSocket } from 'ws';
+
+export interface CoopWebSocket extends WebSocket {
+ clientId: string;
+ sessionId: string;
+ nextMoveTimestamp: number;
+}
diff --git a/backend/src/routes/session.route.ts b/backend/src/routes/session.route.ts
index 3ac8525..cc581ef 100644
--- a/backend/src/routes/session.route.ts
+++ b/backend/src/routes/session.route.ts
@@ -1,7 +1,7 @@
-import { createNewSession } from "../config/session-store.js";
+import { createNewSession } from "../config/coop-session-store.js";
import { Request, Response } from "express";
export function createSession(req: Request, res: Response) {
- const session = createNewSession();
- res.status(201).send(session);
+ // const session = createNewSession();
+ res.status(400).send();
}
diff --git a/backend/src/websocket/coop.ts b/backend/src/websocket/coop.ts
new file mode 100644
index 0000000..ce8bfa0
--- /dev/null
+++ b/backend/src/websocket/coop.ts
@@ -0,0 +1,142 @@
+import {
+ addSessionClient,
+ createNewSession,
+ getSession,
+ setSessionState,
+} from "../config/coop-session-store.js";
+import { WebSocketServer } from "ws";
+import { v4 as uuidv4 } from "uuid";
+import { IncomingMessage } from "http";
+import { CoopWebSocket } from "../model/websocket.js";
+import { parse } from "url";
+import {
+ ClientJoinedCoopSessionMessage,
+ CoopSessionCreatedMessage,
+ CoopSessionMessage,
+ CoopSessionMoveMessage,
+ CoopSessionMoveTimeoutMessage,
+ CoopSessionStateChangeMessage,
+} from "../model/actions/coop.js";
+
+export const wss = new WebSocketServer({ noServer: true });
+
+const sendMessageToClient = (
+ client: CoopWebSocket,
+ message: CoopSessionMessage
+) => {
+ if (client.readyState === client.OPEN) {
+ client.send(JSON.stringify(message));
+ console.log(
+ `Sent message to client ${client.clientId}: ${JSON.stringify(message)}}`
+ );
+ }
+};
+
+wss.on("connection", (ws: CoopWebSocket, req: IncomingMessage) => {
+ ws.clientId = uuidv4();
+
+ const url = parse(req.url!, true);
+
+ if (url.query.action === "create") {
+ const session = createNewSession(ws);
+ sendMessageToClient(ws, <CoopSessionCreatedMessage>{
+ type: "create",
+ payload: {
+ sessionId: session.id,
+ },
+ });
+ ws.sessionId = session.id;
+ } else if (url.query.action === "join") {
+ const session = getSession(url.query.sessionId as string);
+ if (!session) {
+ ws.close();
+ return;
+ }
+
+ addSessionClient(url.query.sessionId as string, ws);
+
+ sendMessageToClient(session.host, <ClientJoinedCoopSessionMessage>{
+ type: "join",
+ payload: {
+ numberOfClients: session.clients.length,
+ },
+ });
+ sendMessageToClient(ws, <CoopSessionStateChangeMessage>{
+ type: "state",
+ payload: {
+ state: session.state,
+ },
+ });
+
+ ws.sessionId = session.id;
+ } else {
+ ws.close();
+ return;
+ }
+
+ ws.on("message", (message) => {
+ console.log(`Received message from client ${ws.clientId}: ${message}`);
+ let data: any;
+ try {
+ data = JSON.parse(message.toString());
+ } catch (e) {
+ return;
+ }
+
+ const session = getSession(ws.sessionId);
+ if (!session) {
+ return;
+ }
+
+ const type = data.type as string;
+ const payload = data.payload as any;
+
+ if (!type || !payload) {
+ return;
+ }
+
+ if (type === "move") {
+ if (Date.now() - ws.nextMoveTimestamp < 0) {
+ return;
+ }
+
+ sendMessageToClient(session.host, <CoopSessionMoveMessage>{
+ type: "move",
+ payload: {
+ move: payload.move,
+ },
+ });
+
+ ws.nextMoveTimestamp = Date.now() + 1000;
+
+ sendMessageToClient(ws, <CoopSessionMoveTimeoutMessage>{
+ type: "timeout",
+ payload: {
+ timeoutUntil: ws.nextMoveTimestamp,
+ },
+ });
+ } else if (type === "start") {
+ session.state = "playing";
+
+ [session.host, ...session.clients].forEach((client) => {
+ sendMessageToClient(client, <CoopSessionStateChangeMessage>{
+ type: "state",
+ payload: {
+ state: "playing",
+ },
+ });
+ });
+ } else if (type === "end") {
+ session.state = "finished";
+
+ [session.host, ...session.clients].forEach((client) => {
+ sendMessageToClient(client, <CoopSessionStateChangeMessage>{
+ type: "state",
+ payload: {
+ state: "finished",
+ },
+ });
+ });
+ }
+ });
+});
diff --git a/backend/src/websocket/game.ts b/backend/src/websocket/game.ts
deleted file mode 100644
index c37b1c5..0000000
--- a/backend/src/websocket/game.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { Server } from "http";
-import { addSessionClient, getSession, setSessionHost, setSessionState } from "../config/session-store.js";
-import { WebSocketServer } from "ws";
-import { v4 as uuidv4 } from "uuid";
-
-const wss = new WebSocketServer({ noServer: true });
-
-const sendToClient = (clientId: string, message: any) => {
- wss.clients.forEach((client: any) => {
- if (client.clientId === clientId) {
- client.send(JSON.stringify(message));
- }
- });
-};
-
-const broadcastToClients = (clientIds: string[], message: any) => {
- wss.clients.forEach((client: any) => {
- if (clientIds.includes(client.clientId)) {
- client.send(JSON.stringify(message));
- }
- });
-};
-
-export const createWebsocketServer = (server: Server): WebSocketServer => {
- server.on("upgrade", (req, socket, head) => {
- wss.handleUpgrade(req, socket, head, (ws) => {
- wss.emit("connection", ws, req);
- });
- });
-
- wss.on("connection", (ws: any) => {
- ws.clientId = uuidv4();
-
- ws.on("message", (message) => {
- console.log("received: %s", message);
- let data;
- try {
- data = JSON.parse(message.toString());
- } catch (e) {
- console.log("Invalid JSON");
- return;
- }
-
- if (data.action === "host") {
- setSessionHost(data.sessionId, ws.clientId);
- } else if (data.action === "move") {
- const session = getSession(data.sessionId);
- if (!session) {
- return;
- }
-
- sendToClient(session.host!, {
- action: "move",
- move: data.move,
- });
- } else if (data.action === "join") {
- const session = getSession(data.sessionId);
- if (!session) {
- return;
- }
-
- addSessionClient(data.sessionId, ws.clientId);
-
- sendToClient(session.host!, {
- action: "join",
- clients: session.clients.length,
- });
-
- ws.send(JSON.stringify({
- action: "state",
- state: session.state,
- }));
- } else if (data.action === "start") {
- const session = getSession(data.sessionId);
- if (!session) {
- return;
- }
-
- setSessionState(data.sessionId, "playing");
-
- sendToClient(session.host!, {
- action: "state",
- state: "playing",
- });
-
- broadcastToClients(session.clients, {
- action: "state",
- state: "playing",
- });
- }
- });
- });
-
- return wss;
-};
-
-export default createWebsocketServer;
diff --git a/backend/src/websocket/websocket-router.ts b/backend/src/websocket/websocket-router.ts
new file mode 100644
index 0000000..0b11985
--- /dev/null
+++ b/backend/src/websocket/websocket-router.ts
@@ -0,0 +1,18 @@
+import { Server } from "http";
+import { wss as gameServer } from "./coop.js";
+
+export const createWebsocketServer = (server: Server) => {
+
+ server.on("upgrade", (req, socket, head) => {
+ if (req.url?.startsWith("/coop")) {
+ gameServer.handleUpgrade(req, socket, head, (ws) => {
+ gameServer.emit("connection", ws, req);
+ });
+ } else {
+ socket.destroy();
+ }
+ });
+
+};
+
+export default createWebsocketServer;
diff --git a/frontend/src/views/HostView.vue b/frontend/src/views/HostView.vue
index 8d6a6b9..adc9c05 100644
--- a/frontend/src/views/HostView.vue
+++ b/frontend/src/views/HostView.vue
@@ -16,46 +16,33 @@ function startGame() {
return;
}
socket.send(JSON.stringify({
- action: 'start',
- sessionId: code.value,
+ type: 'start',
+ payload: {}
}));
}
-fetch(`${import.meta.env.VITE_BACKEND_BASE_URL}/session`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- }
-).then(async (response) => {
- const data: any = await response.json();
- code.value = data.id;
- url.value = `${import.meta.env.VITE_FRONTEND_BASE_URL}/join/${data.id}`;
- waitingForCode.value = false;
- openSocket(data.id);
-});
+socket = new WebSocket(`${import.meta.env.VITE_BACKEND_WS_URL}/coop?action=create`);
-function openSocket(id: string) {
- socket = new WebSocket(`${import.meta.env.VITE_BACKEND_WS_URL}`);
+socket.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ const type = data.type;
+ const payload = data.payload;
- socket.onopen = () => {
- socket.send(JSON.stringify({
- action: 'host',
- sessionId: id,
- }));
- };
- socket.onmessage = (event) => {
- const data = JSON.parse(event.data);
- if (data.action === 'join') {
- numClients.value = data.clients;
- } else if (data.action === 'state' && data.state === 'playing') {
- started.value = true;
- } else if (data.action === 'move') {
- inputQueue.value.push(data.move);
- }
- };
-}
+ if (type === 'create') {
+ code.value = payload.sessionId;
+ waitingForCode.value = false;
+ url.value = `${import.meta.env.VITE_FRONTEND_BASE_URL}/join/${payload.sessionId}`;
+
+ } else if (type === 'join') {
+ numClients.value = payload.numberOfClients;
+
+ } else if (type === 'state') {
+ started.value = payload.state === 'playing';
+
+ } else if (type === 'move') {
+ inputQueue.value.push(payload.move);
+ }
+};
</script>
<template>
diff --git a/frontend/src/views/JoinView.vue b/frontend/src/views/JoinView.vue
index 83421ee..d081512 100644
--- a/frontend/src/views/JoinView.vue
+++ b/frontend/src/views/JoinView.vue
@@ -10,20 +10,35 @@ const connected = ref(false);
const gameState = ref('');
const controlsLocked = ref(false);
-const socket = new WebSocket(`${import.meta.env.VITE_BACKEND_WS_URL}`);
-socket.onopen = () => {
- socket.send(JSON.stringify({
- action: 'join',
- sessionId: sessionId,
- }));
-};
+let moveTimeout: number;
+
+const socket = new WebSocket(`${import.meta.env.VITE_BACKEND_WS_URL}/coop?action=join&sessionId=${sessionId}`);
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
- if (data.action === 'state') {
- gameState.value = data.state;
+ const type = data.type;
+ const payload = data.payload;
+
+ if (type === 'state') {
+ gameState.value = payload.state;
joiningGame.value = false;
connected.value = true;
}
+
+ if (type === 'timeout') {
+ const timeout = payload.timeoutUntil - Date.now();
+ if (timeout < 0) {
+ controlsLocked.value = false;
+ return;
+ }
+
+ controlsLocked.value = true;
+ if (moveTimeout) {
+ clearTimeout(moveTimeout);
+ }
+ moveTimeout = setTimeout(() => {
+ controlsLocked.value = false;
+ }, timeout);
+ }
};
function sendMove(move: string) {
@@ -31,13 +46,11 @@ function sendMove(move: string) {
return;
}
controlsLocked.value = true;
- setTimeout(() => {
- controlsLocked.value = false;
- }, 1000);
socket.send(JSON.stringify({
- action: 'move',
- sessionId: sessionId,
- move: move,
+ type: 'move',
+ payload: {
+ move
+ }
}));
}