mirror of
https://github.com/danbulant/slightlyComplicatedTicTacToe
synced 2026-07-07 12:10:36 +00:00
working multiplayer for desktop
This commit is contained in:
parent
9dc60925f0
commit
c7df814200
9 changed files with 318 additions and 48 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { quadOut } from "svelte/easing";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
import Move from "./move.svelte";
|
import Move from "./move.svelte";
|
||||||
|
|
||||||
export var self: 1 | 2 = 1;
|
export var self: 1 | 2 = 1;
|
||||||
|
|
@ -72,6 +74,24 @@
|
||||||
updateContainerStates();
|
updateContainerStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPlayerMove(player: 1 | 2, i: number, j: number) {
|
||||||
|
if(moves.find(move => move.i == i && move.j == j)) {
|
||||||
|
console.error("DESYNC! Repeated move", player, i, j);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(currentContainer !== i) {
|
||||||
|
console.error("DESYNC! Invalid move (container not active)!", player, i, j, "current container:", i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moves.push({ p: player, i, j });
|
||||||
|
moves = moves;
|
||||||
|
|
||||||
|
updateContainerStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { addPlayerMove };
|
||||||
|
|
||||||
function updateContainerStates() {
|
function updateContainerStates() {
|
||||||
for(var i in containerStates) {
|
for(var i in containerStates) {
|
||||||
if(containerStates[i]) continue;
|
if(containerStates[i]) continue;
|
||||||
|
|
@ -138,9 +158,15 @@
|
||||||
moves = [];
|
moves = [];
|
||||||
containerStates = new Array(9).fill(0);
|
containerStates = new Array(9).fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function check(e: MouseEvent) {
|
||||||
|
if(twoPlayer) return;
|
||||||
|
var confirmed = confirm("Are you sure you want to quit?");
|
||||||
|
if(!confirmed) return e.preventDefault() || false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href="/" class="arrow-back fixed top-0 left-0 w-4 h-4 m-4 p-2 transform transition-transform hover:-translate-x-1">
|
<a href="/" on:click={check} class="arrow-back fixed top-0 left-0 w-4 h-4 m-4 p-2 transform transition-transform hover:-translate-x-1">
|
||||||
<svg width="16" height="16">
|
<svg width="16" height="16">
|
||||||
<line y1="50%" x1="0" y2="50%" x2="100%" stroke="currentColor" stroke-width="2" />
|
<line y1="50%" x1="0" y2="50%" x2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
<line y1="50%" x1="0" y2="100%" x2="50%" stroke="currentColor" stroke-width="2" />
|
<line y1="50%" x1="0" y2="100%" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
|
@ -148,18 +174,20 @@
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div on:click={reset} 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">
|
{#if !twoPlayer}
|
||||||
<svg fill="currentColor" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
<div on:click={reset} 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">
|
||||||
viewBox="0 0 489.645 489.645" xml:space="preserve" class="w-full h-full">
|
<svg fill="currentColor" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
<g>
|
viewBox="0 0 489.645 489.645" xml:space="preserve" class="w-full h-full">
|
||||||
<path d="M460.656,132.911c-58.7-122.1-212.2-166.5-331.8-104.1c-9.4,5.2-13.5,16.6-8.3,27c5.2,9.4,16.6,13.5,27,8.3
|
<g>
|
||||||
c99.9-52,227.4-14.9,276.7,86.3c65.4,134.3-19,236.7-87.4,274.6c-93.1,51.7-211.2,17.4-267.6-70.7l69.3,14.5
|
<path d="M460.656,132.911c-58.7-122.1-212.2-166.5-331.8-104.1c-9.4,5.2-13.5,16.6-8.3,27c5.2,9.4,16.6,13.5,27,8.3
|
||||||
c10.4,2.1,21.8-4.2,23.9-15.6c2.1-10.4-4.2-21.8-15.6-23.9l-122.8-25c-20.6-2-25,16.6-23.9,22.9l15.6,123.8
|
c99.9-52,227.4-14.9,276.7,86.3c65.4,134.3-19,236.7-87.4,274.6c-93.1,51.7-211.2,17.4-267.6-70.7l69.3,14.5
|
||||||
c1,10.4,9.4,17.7,19.8,17.7c12.8,0,20.8-12.5,19.8-23.9l-6-50.5c57.4,70.8,170.3,131.2,307.4,68.2
|
c10.4,2.1,21.8-4.2,23.9-15.6c2.1-10.4-4.2-21.8-15.6-23.9l-122.8-25c-20.6-2-25,16.6-23.9,22.9l15.6,123.8
|
||||||
C414.856,432.511,548.256,314.811,460.656,132.911z"/>
|
c1,10.4,9.4,17.7,19.8,17.7c12.8,0,20.8-12.5,19.8-23.9l-6-50.5c57.4,70.8,170.3,131.2,307.4,68.2
|
||||||
</g>
|
C414.856,432.511,548.256,314.811,460.656,132.911z"/>
|
||||||
</svg>
|
</g>
|
||||||
</div>
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<main class:disabled={overallState} class="flex flex-wrap min-h-100vh min-w-full items-center">
|
<main class:disabled={overallState} class="flex flex-wrap min-h-100vh min-w-full items-center">
|
||||||
<div class="board relative p-8">
|
<div class="board relative p-8">
|
||||||
|
|
@ -242,28 +270,45 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="absolute top-200 left-0 right-0 text-center">
|
{#key currentPlayer}
|
||||||
{#if currentPlayer == 1}
|
<div class="absolute top-200 left-0 right-0 text-center" in:fly={{ delay: 300, duration: 300, easing: quadOut, opacity: 0, y: 30 }} out:fly={{ delay: 0, duration: 300, easing: quadOut, opacity: 0, y: -30 }}>
|
||||||
<svg width="16" height="16" class="text-red-500">
|
{#if currentPlayer == 1}
|
||||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="currentColor" stroke-width="2" />
|
<svg width="16" height="16" class="text-red-500">
|
||||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="currentColor" stroke-width="2" />
|
<line x1="0" y1="0" x2="100%" y2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
</svg>
|
<line x1="100%" y1="0" x2="0" y2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
{:else}
|
</svg>
|
||||||
<svg width="16" height="16" class="text-blue-500">
|
{:else}
|
||||||
<circle cx="50%" cy="50%" r="45%" stroke="currentColor" stroke-width="2" fill="none" />
|
<svg width="16" height="16" class="text-blue-500">
|
||||||
</svg>
|
<circle cx="50%" cy="50%" r="45%" stroke="currentColor" stroke-width="2" fill="none" />
|
||||||
{/if}
|
</svg>
|
||||||
is on turn.
|
{/if}
|
||||||
{#if twoPlayer && self == currentPlayer}
|
is on turn.
|
||||||
<b>It is <span class:text-red-500={currentPlayer == 1} class:text-blue-500={currentPlayer == 2}>YOUR</span> {selfName ? "(" + selfName + ")" : ""} turn.</b>
|
{#if twoPlayer && self == currentPlayer}
|
||||||
{:else if twoPlayer && self != currentPlayer}
|
<b>It is <span class:text-red-500={currentPlayer == 1} class:text-blue-500={currentPlayer == 2}>YOUR</span> {selfName ? "(" + selfName + ")" : ""} turn.</b>
|
||||||
Waiting for {opponentName || "opponent"}...
|
{:else if twoPlayer && self != currentPlayer}
|
||||||
{/if}
|
Waiting for <b class:text-red-500={currentPlayer == 1} class:text-blue-500={currentPlayer == 2}>{opponentName || "opponent"}</b>...
|
||||||
</div>
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info min-w-38 px-4 h-full overflow-y-auto <md:w-full">
|
<div class="info min-w-38 px-4 h-full overflow-y-auto <md:w-full">
|
||||||
<div class="moves">
|
<div class="moves">
|
||||||
|
{#if twoPlayer}
|
||||||
|
<div class="move text-red-500">
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<line x1="0" y1="0" x2="100%" y2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line x1="100%" y1="0" x2="0" y2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
{self == 1 ? (selfName || "you") : (opponentName || "opponent")}
|
||||||
|
</div>
|
||||||
|
<div class="move text-blue-500">
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<circle cx="50%" cy="50%" r="45%" stroke="currentColor" stroke-width="2" fill="none" />
|
||||||
|
</svg>
|
||||||
|
{self == 2 ? (selfName || "you") : (opponentName || "opponent")}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#each moves as move}
|
{#each moves as move}
|
||||||
<Move player={move.p} board={move.i} piece={move.j} on:mouseover={() => hoveredPiece = { i: move.i, j: move.j }} on:mouseout={() => { if(hoveredPiece?.j == move.j && hoveredPiece.i == move.i) hoveredPiece = null }} />
|
<Move player={move.p} board={move.i} piece={move.j} on:mouseover={() => hoveredPiece = { i: move.i, j: move.j }} on:mouseout={() => { if(hoveredPiece?.j == move.j && hoveredPiece.i == move.i) hoveredPiece = null }} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -278,6 +323,9 @@
|
||||||
.info .moves {
|
.info .moves {
|
||||||
columns: 9.5rem auto;
|
columns: 9.5rem auto;
|
||||||
}
|
}
|
||||||
|
.move {
|
||||||
|
@apply p-1 flex gap-2 p-1 items-center leading-none;
|
||||||
|
}
|
||||||
.winner {
|
.winner {
|
||||||
@apply absolute inset-4 pointer-events-none;
|
@apply absolute inset-4 pointer-events-none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
import { quadOut } from "svelte/easing";
|
import { quadOut } from "svelte/easing";
|
||||||
import { fade, fly } from "svelte/transition";
|
import { fade, fly } from "svelte/transition";
|
||||||
import { connection, list } from "./websocket";
|
import { connection, list } from "./websocket";
|
||||||
|
|
@ -6,16 +7,54 @@
|
||||||
const duration = 400;
|
const duration = 400;
|
||||||
|
|
||||||
let createRoomOpen = false;
|
let createRoomOpen = false;
|
||||||
let roomName = "";
|
let roomName = $connection?.name ? `${$connection.name}'s room` : "";
|
||||||
|
|
||||||
function create() {
|
function create() {
|
||||||
console.log("create room", roomName);
|
console.log("create room", roomName);
|
||||||
$connection!.createGame(roomName);
|
$connection!.createGame(roomName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function join(roomName: string) {
|
||||||
|
console.log("join room", roomName);
|
||||||
|
$connection!.join(roomName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
$connection?.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let i = setInterval(() => {
|
||||||
|
$connection?.refreshList();
|
||||||
|
}, 10000); // should usually be synchronized just fine, this is to prevent desynchronization (and also works as a kind of ping)
|
||||||
|
|
||||||
|
return () => clearInterval(i);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<a href="/multiplayer" on:click={disconnect} class="arrow-back fixed top-0 left-0 w-4 h-4 m-4 mt-8 p-2 transform transition-transform hover:-translate-x-1">
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<line y1="50%" x1="0" y2="50%" x2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line y1="50%" x1="0" y2="100%" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line y1="50%" x1="0" y2="0" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div on:click={() => $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">
|
||||||
|
<svg fill="currentColor" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 489.645 489.645" xml:space="preserve" class="w-full h-full">
|
||||||
|
<g>
|
||||||
|
<path d="M460.656,132.911c-58.7-122.1-212.2-166.5-331.8-104.1c-9.4,5.2-13.5,16.6-8.3,27c5.2,9.4,16.6,13.5,27,8.3
|
||||||
|
c99.9-52,227.4-14.9,276.7,86.3c65.4,134.3-19,236.7-87.4,274.6c-93.1,51.7-211.2,17.4-267.6-70.7l69.3,14.5
|
||||||
|
c10.4,2.1,21.8-4.2,23.9-15.6c2.1-10.4-4.2-21.8-15.6-23.9l-122.8-25c-20.6-2-25,16.6-23.9,22.9l15.6,123.8
|
||||||
|
c1,10.4,9.4,17.7,19.8,17.7c12.8,0,20.8-12.5,19.8-23.9l-6-50.5c57.4,70.8,170.3,131.2,307.4,68.2
|
||||||
|
C414.856,432.511,548.256,314.811,460.656,132.911z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="flex flex-col w-100vw h-100vh">
|
<main class="flex flex-col w-100vw h-100vh">
|
||||||
<div class="p-4 flex justify-between border-b border-b-black border-b-solid">
|
<div class="p-4 pl-16 flex justify-between border-b border-b-black border-b-solid">
|
||||||
<h1 in:fly={{ delay: 0, duration, opacity: 0, y: 100, easing: quadOut }}>Rooms</h1>
|
<h1 in:fly={{ delay: 0, duration, opacity: 0, y: 100, easing: quadOut }}>Rooms</h1>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<button in:fly={{ delay: duration * 0.5, duration, opacity: 0, y: 100, easing: quadOut }} disabled>Quick match</button>
|
<button in:fly={{ delay: duration * 0.5, duration, opacity: 0, y: 100, easing: quadOut }} disabled>Quick match</button>
|
||||||
|
|
@ -26,7 +65,7 @@
|
||||||
{#if $list}
|
{#if $list}
|
||||||
<ul>
|
<ul>
|
||||||
{#each $list as room}
|
{#each $list as room}
|
||||||
<li>{room.name}</li>
|
<li on:click={() => join(room.name)}>{room.name}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -51,6 +90,15 @@
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
a {
|
||||||
|
@apply text-black;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
@apply list-none;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
@apply cursor-pointer border-b border-b-solid border-b-gray-300 p-4;
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
@apply w-64 h-10 px-2 border border-gray-300 bg-white mt-4 my-0;
|
@apply w-64 h-10 px-2 border border-gray-300 bg-white mt-4 my-0;
|
||||||
}
|
}
|
||||||
|
|
@ -72,4 +120,7 @@
|
||||||
dialog[open] {
|
dialog[open] {
|
||||||
@apply block fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-0 m-0;
|
@apply block fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-0 m-0;
|
||||||
}
|
}
|
||||||
|
* {
|
||||||
|
@apply box-border select-none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -18,6 +18,14 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<a href="/" class="arrow-back fixed top-0 left-0 w-4 h-4 m-4 p-2 transform transition-transform hover:-translate-x-1">
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<line y1="50%" x1="0" y2="50%" x2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line y1="50%" x1="0" y2="100%" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line y1="50%" x1="0" y2="0" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
{#if shown}
|
{#if shown}
|
||||||
<main>
|
<main>
|
||||||
<div class="top">
|
<div class="top">
|
||||||
|
|
@ -33,6 +41,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
a {
|
||||||
|
@apply text-black;
|
||||||
|
}
|
||||||
* {
|
* {
|
||||||
@apply box-border;
|
@apply box-border;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
client/src/lib/wait.svelte
Normal file
35
client/src/lib/wait.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { quadOut } from "svelte/easing";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import { room } from "./websocket";
|
||||||
|
|
||||||
|
const duration = 400;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href="/multiplayer" class="arrow-back fixed top-0 left-0 w-4 h-4 m-4 p-2 transform transition-transform hover:-translate-x-1">
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<line y1="50%" x1="0" y2="50%" x2="100%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line y1="50%" x1="0" y2="100%" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
<line y1="50%" x1="0" y2="0" x2="50%" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1 in:fly={{ delay: 0, duration, opacity: 0, y: 100, easing: quadOut }}>Waiting for an opponent...</h1>
|
||||||
|
<p in:fly={{ delay: duration * 0.5, duration, opacity: 0, y: 100, easing: quadOut }}>Currently in room <code>{$room?.name}</code>.</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
@apply w-100vw h-100vh flex flex-col items-center justify-center;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
@apply text-gray-400;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
@apply text-black;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
@apply text-black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { writable, type Writable } from "svelte/store";
|
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 {
|
export class WebsocketConnection extends EventTarget {
|
||||||
ws: WebSocket;
|
ws: WebSocket;
|
||||||
roomName: string | null = null;
|
roomName: string | null = null;
|
||||||
|
|
@ -30,7 +36,7 @@ export class WebsocketConnection extends EventTarget {
|
||||||
list.set(null);
|
list.set(null);
|
||||||
});
|
});
|
||||||
this.ws.addEventListener("error", (e) => {
|
this.ws.addEventListener("error", (e) => {
|
||||||
console.error("WS error");
|
console.error("WS error", e);
|
||||||
this.addError("Connection error");
|
this.addError("Connection error");
|
||||||
connection.set(null);
|
connection.set(null);
|
||||||
room.set(null);
|
room.set(null);
|
||||||
|
|
@ -43,6 +49,7 @@ export class WebsocketConnection extends EventTarget {
|
||||||
case "join": {
|
case "join": {
|
||||||
messages.update(t => { t.push({ type: "system", content: `${msg.client} joined`});return t})
|
messages.update(t => { t.push({ type: "system", content: `${msg.client} joined`});return t})
|
||||||
this.players.add(msg.client);
|
this.players.add(msg.client);
|
||||||
|
room.update(t => { t!.count = this.players.size; return t });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "joined": {
|
case "joined": {
|
||||||
|
|
@ -55,7 +62,8 @@ export class WebsocketConnection extends EventTarget {
|
||||||
this.roomHost = msg.host;
|
this.roomHost = msg.host;
|
||||||
room.set({
|
room.set({
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
host: msg.host
|
host: msg.host,
|
||||||
|
count: clients.length
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +72,8 @@ export class WebsocketConnection extends EventTarget {
|
||||||
this.roomHost = this.name;
|
this.roomHost = this.name;
|
||||||
room.set({
|
room.set({
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
host: this.name
|
host: this.name,
|
||||||
|
count: 1
|
||||||
});
|
});
|
||||||
this.addMessage({ type: "system", content: `${msg.name} created the room`});
|
this.addMessage({ type: "system", content: `${msg.name} created the room`});
|
||||||
break;
|
break;
|
||||||
|
|
@ -75,6 +84,7 @@ export class WebsocketConnection extends EventTarget {
|
||||||
gameData.set(null);
|
gameData.set(null);
|
||||||
}
|
}
|
||||||
this.players.delete(msg.client);
|
this.players.delete(msg.client);
|
||||||
|
room.update(t => { t!.count = this.players.size; return t });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "host": {
|
case "host": {
|
||||||
|
|
@ -96,6 +106,30 @@ export class WebsocketConnection extends EventTarget {
|
||||||
listLoading.set(false);
|
listLoading.set(false);
|
||||||
break;
|
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": {
|
case "error": {
|
||||||
console.error(msg.e);
|
console.error(msg.e);
|
||||||
this.addError(msg.e);
|
this.addError(msg.e);
|
||||||
|
|
@ -143,6 +177,10 @@ export class WebsocketConnection extends EventTarget {
|
||||||
send(data: any) {
|
send(data: any) {
|
||||||
this.ws.send(data);
|
this.ws.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leave() {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorMessage {
|
interface ErrorMessage {
|
||||||
|
|
@ -162,6 +200,6 @@ interface SystemMessage {
|
||||||
export const connection: Writable<WebsocketConnection | null> = writable(null);
|
export const connection: Writable<WebsocketConnection | null> = writable(null);
|
||||||
export const list: Writable<{ name: string }[] | null> = writable(null);
|
export const list: Writable<{ name: string }[] | null> = writable(null);
|
||||||
export const listLoading = writable(true);
|
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 messages: Writable<(UserMessage | ErrorMessage | SystemMessage)[]> = writable([]);
|
||||||
export const gameData: Writable<{ log: { p: string, i: number, j: number }[] }|null> = writable(null);
|
export const gameData: Writable<{ log: { p: string, i: number, j: number }[] }|null> = writable(null);
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
<svelte:head>
|
||||||
|
<title>Slightly complicated tictactoe</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<main class="flex items-center justify-center flex-col">
|
<main class="flex items-center justify-center flex-col">
|
||||||
<div class="chooser">
|
<div class="chooser">
|
||||||
|
|
@ -50,9 +53,18 @@
|
||||||
.chooser > a {
|
.chooser > a {
|
||||||
@apply text-black no-underline cursor-pointer w-full p-8 border rounded-lg border-gray-400 border-solid;
|
@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 {
|
.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;
|
@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 {
|
.icon {
|
||||||
@apply h-20 w-20 mr-4;
|
@apply h-20 w-20 mr-4;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,8 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Slightly complicated tictactoe</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Game />
|
<Game />
|
||||||
|
|
@ -1,14 +1,61 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Game from "$lib/game.svelte";
|
import Game from "$lib/game.svelte";
|
||||||
import List from "$lib/list.svelte";
|
import List from "$lib/list.svelte";
|
||||||
import NameChoose from "$lib/nameChoose.svelte";
|
import NameChoose from "$lib/nameChoose.svelte";
|
||||||
import { connection, room } from "$lib/websocket";
|
import Wait from "$lib/wait.svelte";
|
||||||
|
import { connection, MoveEvent, room } from "$lib/websocket";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
var addPlayerMove: (p: number, i: number, j: number) => void;
|
||||||
|
|
||||||
|
function addSelfMove(e: any) {
|
||||||
|
$connection?.broadcast({
|
||||||
|
t: "move",
|
||||||
|
i: e.detail.i,
|
||||||
|
j: e.detail.j
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
return () => {
|
||||||
|
$connection?.leave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let addedEventListener = false;
|
||||||
|
$: {
|
||||||
|
if($connection && !addedEventListener) {
|
||||||
|
function addOtherMove(event: MoveEvent) {
|
||||||
|
console.log("Received other move", event.i, event.j);
|
||||||
|
// note the changed order between this and self - we need to set the opponent here!
|
||||||
|
addPlayerMove($room?.host === $connection?.name ? 2 : 1, event.i, event.j);
|
||||||
|
}
|
||||||
|
console.log("Adding event listener");
|
||||||
|
$connection.addEventListener("move", addOtherMove as any);
|
||||||
|
addedEventListener = true;
|
||||||
|
} else if(!$connection && addedEventListener) {
|
||||||
|
addedEventListener = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Slightly complicated tictactoe</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
{#if !$connection}
|
{#if !$connection}
|
||||||
<NameChoose />
|
<NameChoose />
|
||||||
{:else if !$room}
|
{:else if !$room}
|
||||||
<List />
|
<List />
|
||||||
|
{:else if $room.count < 2}
|
||||||
|
<Wait />
|
||||||
{:else}
|
{:else}
|
||||||
<Game />
|
<Game
|
||||||
|
twoPlayer
|
||||||
|
self={$room.host === $connection.name ? 1 : 2}
|
||||||
|
opponentName={$connection.name}
|
||||||
|
selfName={[...$connection.players.values()].find(t => t !== $connection?.name)}
|
||||||
|
on:move={addSelfMove}
|
||||||
|
bind:addPlayerMove
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -23,6 +23,18 @@ const rooms = new Map();
|
||||||
*/
|
*/
|
||||||
const clients = 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")
|
require("uWebSockets.js")
|
||||||
.App({})
|
.App({})
|
||||||
.ws("/", {
|
.ws("/", {
|
||||||
|
|
@ -39,11 +51,15 @@ require("uWebSockets.js")
|
||||||
name.length < 2 ||
|
name.length < 2 ||
|
||||||
name.length > 64 ||
|
name.length > 64 ||
|
||||||
!name.trim()
|
!name.trim()
|
||||||
)
|
) {
|
||||||
|
console.log("Invalid name");
|
||||||
return res.end("invalid_name");
|
return res.end("invalid_name");
|
||||||
|
}
|
||||||
name = name.trim();
|
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");
|
return res.end("name_used");
|
||||||
|
}
|
||||||
/* This immediately calls open handler, you must not use res after this call */
|
/* This immediately calls open handler, you must not use res after this call */
|
||||||
res.upgrade(
|
res.upgrade(
|
||||||
{
|
{
|
||||||
|
|
@ -120,8 +136,15 @@ require("uWebSockets.js")
|
||||||
};
|
};
|
||||||
rooms.set(room.name, room);
|
rooms.set(room.name, room);
|
||||||
client.room = room;
|
client.room = room;
|
||||||
|
for(let [, cclient] of clients) {
|
||||||
|
if(!cclient.room) {
|
||||||
|
cclient.connection.send(
|
||||||
|
JSON.stringify({ t: "room_created", name })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return ws.send(
|
return ws.send(
|
||||||
JSON.stringify({ t: "create", name: name })
|
JSON.stringify({ t: "create", name })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "leave": {
|
case "leave": {
|
||||||
|
|
@ -139,7 +162,7 @@ require("uWebSockets.js")
|
||||||
);
|
);
|
||||||
room.clients.splice(room.clients.indexOf(client), 1);
|
room.clients.splice(room.clients.indexOf(client), 1);
|
||||||
if (room.clients.length === 0) {
|
if (room.clients.length === 0) {
|
||||||
rooms.delete(room.name);
|
deleteRoom(room.name);
|
||||||
} else if (room.host == ws) {
|
} else if (room.host == ws) {
|
||||||
room.host = room.clients[0];
|
room.host = room.clients[0];
|
||||||
room.clients.forEach((client) =>
|
room.clients.forEach((client) =>
|
||||||
|
|
@ -233,12 +256,13 @@ require("uWebSockets.js")
|
||||||
JSON.stringify({ t: "error", e: "not_in_room" })
|
JSON.stringify({ t: "error", e: "not_in_room" })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let clientName = client.name
|
||||||
room.clients.forEach((client) => {
|
room.clients.forEach((client) => {
|
||||||
client.connection.send(
|
client.connection.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
t: "broadcast",
|
t: "broadcast",
|
||||||
client: client.name,
|
client: clientName,
|
||||||
data: data.d,
|
d: data.d,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -274,7 +298,7 @@ require("uWebSockets.js")
|
||||||
if (room) {
|
if (room) {
|
||||||
room.clients.splice(room.clients.indexOf(client), 1);
|
room.clients.splice(room.clients.indexOf(client), 1);
|
||||||
if (room.clients.length === 0) {
|
if (room.clients.length === 0) {
|
||||||
rooms.delete(room.name);
|
deleteRoom(room.name);
|
||||||
} else if (room.host == ws) {
|
} else if (room.host == ws) {
|
||||||
room.host = room.clients[0];
|
room.host = room.clients[0];
|
||||||
room.clients.forEach((client) =>
|
room.clients.forEach((client) =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue