From c7df81420026cadc23f13183d03a8a00f04a7426 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Fri, 20 Jan 2023 19:38:40 +0100 Subject: [PATCH] working multiplayer for desktop --- client/src/lib/game.svelte | 110 +++++++++++++++------ client/src/lib/list.svelte | 57 ++++++++++- client/src/lib/nameChoose.svelte | 11 +++ client/src/lib/wait.svelte | 35 +++++++ client/src/lib/websocket.ts | 46 ++++++++- client/src/routes/+page.svelte | 12 +++ client/src/routes/localplay/+page.svelte | 4 + client/src/routes/multiplayer/+page.svelte | 53 +++++++++- server/src/index.js | 38 +++++-- 9 files changed, 318 insertions(+), 48 deletions(-) create mode 100644 client/src/lib/wait.svelte diff --git a/client/src/lib/game.svelte b/client/src/lib/game.svelte index f41e17e..38d8a60 100644 --- a/client/src/lib/game.svelte +++ b/client/src/lib/game.svelte @@ -1,5 +1,7 @@ - + @@ -148,18 +174,20 @@ -
- - - - - -
+{#if !twoPlayer} +
+ + + + + +
+{/if}
@@ -242,28 +270,45 @@
{/if} -
- {#if currentPlayer == 1} - - - - - {:else} - - - - {/if} - is on turn. - {#if twoPlayer && self == currentPlayer} - It is YOUR {selfName ? "(" + selfName + ")" : ""} turn. - {:else if twoPlayer && self != currentPlayer} - Waiting for {opponentName || "opponent"}... - {/if} -
+ {#key currentPlayer} +
+ {#if currentPlayer == 1} + + + + + {:else} + + + + {/if} + is on turn. + {#if twoPlayer && self == currentPlayer} + It is YOUR {selfName ? "(" + selfName + ")" : ""} turn. + {:else if twoPlayer && self != currentPlayer} + Waiting for {opponentName || "opponent"}... + {/if} +
+ {/key}
+ {#if twoPlayer} +
+ + + + + {self == 1 ? (selfName || "you") : (opponentName || "opponent")} +
+
+ + + + {self == 2 ? (selfName || "you") : (opponentName || "opponent")} +
+ {/if} {#each moves as move} hoveredPiece = { i: move.i, j: move.j }} on:mouseout={() => { if(hoveredPiece?.j == move.j && hoveredPiece.i == move.i) hoveredPiece = null }} /> {/each} @@ -278,6 +323,9 @@ .info .moves { columns: 9.5rem auto; } + .move { + @apply p-1 flex gap-2 p-1 items-center leading-none; + } .winner { @apply absolute inset-4 pointer-events-none; } diff --git a/client/src/lib/list.svelte b/client/src/lib/list.svelte index d950064..db8c8f9 100644 --- a/client/src/lib/list.svelte +++ b/client/src/lib/list.svelte @@ -1,4 +1,5 @@ + + + + + + + + +
$connection?.refreshList()} on:keydown={() => $connection?.refreshList()} class="reload fixed top-0 left-10 w-4 h-4 m-4 p-2 transform transition-transform rotate-180 hover:rotate-360 active:rotate-540"> + + + + + +
+
-
+

Rooms

@@ -26,7 +65,7 @@ {#if $list}
    {#each $list as room} -
  • {room.name}
  • +
  • join(room.name)}>{room.name}
  • {/each}
{:else} @@ -51,6 +90,15 @@ \ No newline at end of file diff --git a/client/src/lib/nameChoose.svelte b/client/src/lib/nameChoose.svelte index 7a464f0..298e54f 100644 --- a/client/src/lib/nameChoose.svelte +++ b/client/src/lib/nameChoose.svelte @@ -18,6 +18,14 @@ } + + + + + + + + {#if shown}
@@ -33,6 +41,9 @@ {/if} \ No newline at end of file diff --git a/client/src/lib/websocket.ts b/client/src/lib/websocket.ts index 87a9e48..cd22ba5 100644 --- a/client/src/lib/websocket.ts +++ b/client/src/lib/websocket.ts @@ -1,5 +1,11 @@ import { writable, type Writable } from "svelte/store"; +export class MoveEvent extends Event { + constructor(public i: number, public j: number) { + super("move"); + } +} + export class WebsocketConnection extends EventTarget { ws: WebSocket; roomName: string | null = null; @@ -30,7 +36,7 @@ export class WebsocketConnection extends EventTarget { list.set(null); }); this.ws.addEventListener("error", (e) => { - console.error("WS error"); + console.error("WS error", e); this.addError("Connection error"); connection.set(null); room.set(null); @@ -43,6 +49,7 @@ export class WebsocketConnection extends EventTarget { case "join": { messages.update(t => { t.push({ type: "system", content: `${msg.client} joined`});return t}) this.players.add(msg.client); + room.update(t => { t!.count = this.players.size; return t }); break; } case "joined": { @@ -55,7 +62,8 @@ export class WebsocketConnection extends EventTarget { this.roomHost = msg.host; room.set({ name: msg.name, - host: msg.host + host: msg.host, + count: clients.length }); break; } @@ -64,7 +72,8 @@ export class WebsocketConnection extends EventTarget { this.roomHost = this.name; room.set({ name: msg.name, - host: this.name + host: this.name, + count: 1 }); this.addMessage({ type: "system", content: `${msg.name} created the room`}); break; @@ -75,6 +84,7 @@ export class WebsocketConnection extends EventTarget { gameData.set(null); } this.players.delete(msg.client); + room.update(t => { t!.count = this.players.size; return t }); break; } case "host": { @@ -96,6 +106,30 @@ export class WebsocketConnection extends EventTarget { listLoading.set(false); break; } + case "room_created": { + list.update(t => { t?.push({ name: msg.name}); return t }); + break; + } + case "room_deleted": { + list.update(t => { + if(!t) return t; + var i = t.findIndex(t => t.name == msg.name); + if(i == -1) return t; + t.splice(i, 1); + return t; + }); + break; + } + case "broadcast": { + if(msg.client == this.name) break; + switch (msg.d.t) { + case "move": { + console.log("Dispatching move event", msg.d.i, msg.d.j) + this.dispatchEvent(new MoveEvent(msg.d.i, msg.d.j)); + } + } + break; + } case "error": { console.error(msg.e); this.addError(msg.e); @@ -143,6 +177,10 @@ export class WebsocketConnection extends EventTarget { send(data: any) { this.ws.send(data); } + + leave() { + this.ws.close(); + } } interface ErrorMessage { @@ -162,6 +200,6 @@ interface SystemMessage { export const connection: Writable = writable(null); export const list: Writable<{ name: string }[] | null> = writable(null); export const listLoading = writable(true); -export const room: Writable<{ name: string, host: string } | null> = writable(null); +export const room: Writable<{ name: string, host: string, count: number } | null> = writable(null); export const messages: Writable<(UserMessage | ErrorMessage | SystemMessage)[]> = writable([]); export const gameData: Writable<{ log: { p: string, i: number, j: number }[] }|null> = writable(null); \ No newline at end of file diff --git a/client/src/routes/+page.svelte b/client/src/routes/+page.svelte index 19fe789..5f48696 100644 --- a/client/src/routes/+page.svelte +++ b/client/src/routes/+page.svelte @@ -1,3 +1,6 @@ + + Slightly complicated tictactoe +
@@ -50,9 +53,18 @@ .chooser > a { @apply text-black no-underline cursor-pointer w-full p-8 border rounded-lg border-gray-400 border-solid; } + .chooser > a:hover { + @apply bg-black/10; + } .rules { @apply cursor-not-allowed text-gray-500 flex justify-center items-center w-full my-8 p-4 border rounded-lg border-gray-400 border-solid; } + .rules:hover { + @apply bg-red-500/3; + } + .rules:active { + @apply bg-red-500/10; + } .icon { @apply h-20 w-20 mr-4; } diff --git a/client/src/routes/localplay/+page.svelte b/client/src/routes/localplay/+page.svelte index 1e30fea..fde1e13 100644 --- a/client/src/routes/localplay/+page.svelte +++ b/client/src/routes/localplay/+page.svelte @@ -3,4 +3,8 @@ + + Slightly complicated tictactoe + + \ No newline at end of file diff --git a/client/src/routes/multiplayer/+page.svelte b/client/src/routes/multiplayer/+page.svelte index 6390e8f..0a265ee 100644 --- a/client/src/routes/multiplayer/+page.svelte +++ b/client/src/routes/multiplayer/+page.svelte @@ -1,14 +1,61 @@ - + + Slightly complicated tictactoe + + {#if !$connection} {:else if !$room} +{:else if $room.count < 2} + {:else} - + t !== $connection?.name)} + on:move={addSelfMove} + bind:addPlayerMove + /> {/if} \ No newline at end of file diff --git a/server/src/index.js b/server/src/index.js index ef47226..f651b3b 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -23,6 +23,18 @@ const rooms = new Map(); */ const clients = new Map(); +function deleteRoom(room) { + rooms.delete(room); + + for(let [, client] of clients) { + if(!client.room) { + client.connection.send( + JSON.stringify({ t: "room_deleted", name: room }) + ) + } + } +} + require("uWebSockets.js") .App({}) .ws("/", { @@ -39,11 +51,15 @@ require("uWebSockets.js") name.length < 2 || name.length > 64 || !name.trim() - ) + ) { + console.log("Invalid name"); return res.end("invalid_name"); + } name = name.trim(); - if ([...clients.values()].find((client) => client.name === name)) + if ([...clients.values()].find((client) => client.name === name)) { + console.log("Duplicate name"); return res.end("name_used"); + } /* This immediately calls open handler, you must not use res after this call */ res.upgrade( { @@ -120,8 +136,15 @@ require("uWebSockets.js") }; rooms.set(room.name, room); client.room = room; + for(let [, cclient] of clients) { + if(!cclient.room) { + cclient.connection.send( + JSON.stringify({ t: "room_created", name }) + ); + } + } return ws.send( - JSON.stringify({ t: "create", name: name }) + JSON.stringify({ t: "create", name }) ); } case "leave": { @@ -139,7 +162,7 @@ require("uWebSockets.js") ); room.clients.splice(room.clients.indexOf(client), 1); if (room.clients.length === 0) { - rooms.delete(room.name); + deleteRoom(room.name); } else if (room.host == ws) { room.host = room.clients[0]; room.clients.forEach((client) => @@ -233,12 +256,13 @@ require("uWebSockets.js") JSON.stringify({ t: "error", e: "not_in_room" }) ); + let clientName = client.name room.clients.forEach((client) => { client.connection.send( JSON.stringify({ t: "broadcast", - client: client.name, - data: data.d, + client: clientName, + d: data.d, }) ); }); @@ -274,7 +298,7 @@ require("uWebSockets.js") if (room) { room.clients.splice(room.clients.indexOf(client), 1); if (room.clients.length === 0) { - rooms.delete(room.name); + deleteRoom(room.name); } else if (room.host == ws) { room.host = room.clients[0]; room.clients.forEach((client) =>