feat: Initial commit

This commit is contained in:
Ashhhleyyy 2022-08-13 16:08:44 +01:00
commit bf11761bc8
Signed by: ash
GPG key ID: 83B789081A0878FB
37 changed files with 6853 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

32
.prettierignore Normal file
View file

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# lockfile
pnpm-lock.yaml
# CI
.pnpm-store
.drone.yml
node_modules
dist
dist-ssr
*.local
.next/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"jsxSingleQuote": true,
"semi": true,
"tabWidth": 4,
"useTabs": false,
"trailingComma": "es5",
"singleQuote": true
}

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# Doodly
A skribbl.io clone that doesn't include builtin drawing.

16
index.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Draw with friends">
<title>Doodly</title>
<link rel="icon" href="/favicon.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

34
package.json Normal file
View file

@ -0,0 +1,34 @@
{
"name": "doodly",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"dev:server": "node --loader ts-node/esm src/server/main.ts",
"format:check": "prettier --check .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
"prettier": "^2.7.1",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"ts-node": "^10.9.1",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.6",
"vite-plugin-pwa": "^0.12.3"
},
"dependencies": {
"@fontsource/noto-sans": "^4.5.11",
"bulma": "^0.9.4",
"socket.io": "^4.5.1",
"socket.io-client": "^4.5.1"
}
}

3522
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/favicon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

2
public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

14
squircle.svg Normal file
View file

@ -0,0 +1,14 @@
<svg viewbox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path d="
M 0, 75
C 0, 18.75 18.75, 0 75, 0
S 150, 18.75 150, 75
131.25, 150 75, 150
0, 131.25 0, 75
" fill="#FADB5F" transform="rotate(
0,
100,
100
) translate(
25
25
)"></path></svg>

After

Width:  |  Height:  |  Size: 451 B

171
src/App.svelte Normal file
View file

@ -0,0 +1,171 @@
<script lang="ts">
import { io, type Socket } from "socket.io-client";
import { onDestroy } from "svelte";
import WaitingLobby from "./client/WaitingLobby.svelte";
import JoinPage from "./client/JoinPage.svelte";
import type { ChatMessage, ClientToServerEvents, GameState, Player, ServerToClientEvents, GameOver as GameOverData } from "./lib/socket";
import SelectWord from "./client/SelectWord.svelte";
import Card from "./components/Card.svelte";
import ChatWindow from "./client/ChatWindow.svelte";
import GuessPage from "./client/GuessPage.svelte";
import GameOver from "./client/GameOver.svelte";
let socket: Socket<ServerToClientEvents, ClientToServerEvents> | null = null;
let connected = false;
let username = '';
let gameId = '';
let error = '';
let host = false;
let players: Player[] = [];
let gameState: GameState = 'waiting';
let wordOptions: string[] = null;
let chat: ChatMessage[] = [];
let gameOver: GameOverData = null;
function joinGame(uname: string, gameId: string) {
players = [];
socket = io(`http://${window.location.hostname}:3000/`);
socket.on('connect', () => {
socket.emit('joinGame', gameId, uname, (result) => {
if (result.error === false) {
const { id, isHost, players: p } = result.data;
players = p;
host = isHost;
connected = true;
username = uname;
gameState = 'waiting';
chat = [];
gameOver = null;
} else {
console.log('failed to join', result.message);
socket.disconnect();
socket = null;
connected = false;
error = result.message;
}
});
});
socket.on('disconnect', (reason) => {
connected = false;
error = 'lost connection: ' + reason;
socket.disconnect();
socket = null;
});
socket.on('playerJoined', (username) => {
players.push({
username,
isHost: false,
});
players = players;
});
socket.on('playerLeft', (username) => {
players = players.filter(p => p.username !== username);
});
socket.on('becameHost', (newHost) => {
if (newHost === username) {
host = true;
}
players = players.map(player => {
return {
...player,
isHost: player.username === newHost,
}
});
});
socket.on('waitForWordPick', () => {
gameState = 'waitingForWord';
});
socket.on('pickWord', (words) => {
console.log('pickWord', gameState, words);
gameState = 'selectingWord';
wordOptions = words;
});
socket.on('startDrawing', () => {
gameState = 'drawing';
});
socket.on('startGuessing', () => {
gameState = 'guessing';
});
socket.on('chatMessage', (author, message) => {
chat.reverse();
chat.push({ author, message });
chat.reverse();
chat = chat;
});
socket.on('gameOver', (result) => {
gameState = 'gameOver';
gameOver = result;
});
}
onDestroy(() => {
if (socket) {
socket.disconnect();
}
});
function startGame() {
socket.emit('startGame');
chat = [];
}
</script>
<main>
{#if !connected}
<JoinPage joinGame={joinGame} error={error} loading={socket !== null} on:dismiss-error={() => error = ''}></JoinPage>
{:else}
{#if gameState === 'waiting'}
<WaitingLobby username={username} gameId={gameId} host={host} socket={socket} players={players}></WaitingLobby>
{:else if gameState === 'waitingForWord'}
<Card>
<p>
Waiting for a word to be picked...
</p>
</Card>
{:else if gameState === 'selectingWord'}
<SelectWord words={wordOptions} on:pick-word={(event) => socket.emit('pickWord', event.detail)} />
{:else if gameState === 'guessing'}
<GuessPage on:guess={(e) => socket.emit('guessWord', e.detail, (correct) => {
if (correct) {
gameState = 'guessedCorrectly';
}
})} />
<ChatWindow messages={chat} />
{:else if gameState === 'guessedCorrectly'}
<Card>
<h1>Correct!</h1>
<p>
Waiting for the other players to finish...
</p>
</Card>
<ChatWindow messages={chat} />
{:else if gameState === 'drawing'}
<Card>
<h1>
Start drawing!
</h1>
</Card>
<ChatWindow messages={chat} />
{:else if gameState === 'gameOver'}
<GameOver gameOver={gameOver} isHost={host} {username} on:play-again={startGame} />
{/if}
{/if}
</main>

15
src/app.css Normal file
View file

@ -0,0 +1,15 @@
body {
font-family: 'Noto Sans', sans-serif;
/* background-color: #212;
color: #ded;
box-sizing: border-box; */
}
* {
box-sizing: inherit;
}
/* input, button {
font-family: inherit;
} */

2299
src/assets/wordlist.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
<script lang="ts">
import Card from "../components/Card.svelte";
import type { ChatMessage } from "../lib/socket";
export let messages: ChatMessage[];
</script>
<Card>
<h2>Chat</h2>
<div aria-live="polite">
{#each messages as message}
<p>
<span class="has-text-info">{message.author}</span>:
{message.message}
</p>
{/each}
</div>
</Card>

View file

@ -0,0 +1,80 @@
<script lang="ts">
import Button from "../components/Button.svelte";
import Card from "../components/Card.svelte";
import { createEventDispatcher } from "svelte";
import type { GameOver } from "../lib/socket";
const dispatch = createEventDispatcher();
export let gameOver: GameOver;
export let username: string;
export let isHost: boolean;
</script>
<Card>
<h1>Game over</h1>
<section>
<h2>Details</h2>
<ul>
<li>Drawing: <code>{gameOver.drawer}</code></li>
<li>Word: <code>{gameOver.word}</code></li>
</ul>
</section>
<section class="rankings">
<h2>Rankings</h2>
<table class="table">
<thead>
<tr class="is-flex is-flex-direction-row">
<th><abbr title="Position">Pos</abbr></th>
<th class="is-flex-grow-1">Player</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{#each gameOver.leaderboard as entry, i}
<tr class="is-flex is-flex-direction-row">
<th>{i + 1}</th>
<td class="is-flex-grow-1">
<span class:has-text-primary={entry.username === username}>
{entry.username}
</span>
{#if entry.username === username}
<span class="tag is-primary">
You
</span>
{/if}
</td>
<td>
{(entry.time / 1000).toFixed(1)}
</td>
</tr>
{/each}
</tbody>
</table>
</section>
{#if isHost}
<section>
<h1>
Host options
</h1>
<div class="buttons">
<Button title="Play again" on:click={() => dispatch('play-again')} colour="primary">
Play again
</Button>
</div>
</section>
{/if}
</Card>
<style>
.rankings {
max-width: fit-content;
}
</style>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import Card from "../components/Card.svelte";
import { createEventDispatcher } from "svelte";
import TextInput from "../components/TextInput.svelte";
import Button from "../components/Button.svelte";
import ChatWindow from "./ChatWindow.svelte";
const dispatch = createEventDispatcher<{ guess: string }>();
let composeMessage = '';
function submit(m?: string) {
const guess = m ?? composeMessage;
if (guess !== '') {
dispatch('guess', guess);
}
composeMessage = '';
}
</script>
<Card>
<h1>Guess!</h1>
<TextInput placeholder="Guess a word!" autofocus bind:value={composeMessage} on:submit={(e) => submit(e.detail)} />
<Button title="Guess" colour="primary" on:click={() => submit()}>Guess</Button>
</Card>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import Card from "../components/Card.svelte";
import Button from "../components/Button.svelte";
import TextInput from "../components/TextInput.svelte";
import { createEventDispatcher } from "svelte";
export let joinGame: (username: string, gameId: string) => void;
export let error: string;
export let loading = false;
let username = '';
let gameId = '';
const dispatch = createEventDispatcher();
</script>
<Card>
{#if error !== ''}
<div class="notification is-danger is-light">
<button class="delete" on:click={() => dispatch('dismiss-error')} />
Error: {error}
</div>
{/if}
<TextInput bind:value={username} placeholder="Username" maxlength={32} disabled={loading} />
<TextInput bind:value={gameId} placeholder="Game ID" maxlength={32} disabled={loading} />
<Button title="Join game" on:click={() => joinGame(username, gameId)} colour="primary" {loading}>
Join game
</Button>
</Card>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import Button from "../components/Button.svelte";
import Card from "../components/Card.svelte";
import { createEventDispatcher } from "svelte";
export let words: string[];
const dispatch = createEventDispatcher<{ 'pick-word': string }>();
function pickWord(word: string) {
dispatch('pick-word', word);
}
</script>
<Card>
<h1>Pick a word!</h1>
<div class="buttons">
{#each words as word}
<Button title={word} on:click={() => pickWord(word)} colour="primary">{word}</Button>
{/each}
</div>
</Card>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import type { Socket } from "socket.io-client";
import Button from "../components/Button.svelte";
import Card from "../components/Card.svelte";
import type { ClientToServerEvents, Player, ServerToClientEvents } from "../lib/socket";
export let players: Player[];
export let username: string;
export let gameId: string;
export let host: boolean;
export let socket: Socket<ServerToClientEvents, ClientToServerEvents>;
</script>
<Card>
<h1>
Waiting for
{#if host}
players
{:else}
the host
{/if}
</h1>
<section>
<h2>
Players
</h2>
<ul>
{#each players as player}
<li>
<span class:has-text-info={player.username === username}>
{player.username}
</span>
{#if player.isHost}
<span class="tag is-warning">
Host
</span>
{/if}
</li>
{/each}
</ul>
</section>
{#if host}
<section>
<h2>Game</h2>
<Button title="Start game" colour="primary" on:click={() => socket.emit('startGame')} disabled={players.length < 2}>Start game</Button>
</section>
{/if}
</Card>
<Card>
<h2>How to play</h2>
<p>
A random player will be chosen to pick a word, which they will then have to draw it in front of everyone else.
<br>
All incorrect guesses are broadcast to the chat window for every player though, so be careful!
</p>
</Card>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { BulmaColour } from "./utils";
export let title: string;
export let disabled: boolean = false;
export let colour: BulmaColour = undefined;
export let loading: boolean = false;
const dispatch = createEventDispatcher();
function dispatchClick() {
if (!disabled) dispatch('click');
}
</script>
<button class={`button is-${colour}`} class:is-loading={loading} {disabled} {title} on:click={dispatchClick}>
<slot />
</button>

View file

@ -0,0 +1,7 @@
<div class="container">
<div class="card mt-4">
<div class="card-content content">
<slot />
</div>
</div>
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let placeholder: string;
export let value: string;
export let id: string = undefined;
export let autofocus = false;
export let maxlength: number = undefined;
export let disabled: boolean = false;
const dispatch = createEventDispatcher<{ submit: string }>();
</script>
<!-- svelte-ignore a11y-autofocus -->
<div class="field">
<div class="control">
<input type="text" {id} {placeholder} {autofocus} {maxlength} {disabled} bind:value class="input" on:keyup={(e) => {
if (e.key === 'Enter') {
dispatch('submit', value);
}
}}>
</div>
</div>

1
src/components/utils.ts Normal file
View file

@ -0,0 +1 @@
export type BulmaColour = 'primary' | 'link' | 'success' | 'warning' | 'danger';

43
src/lib/socket.ts Normal file
View file

@ -0,0 +1,43 @@
export interface ServerToClientEvents {
playerJoined: (username: string) => void;
playerLeft: (username: string) => void;
becameHost: (username: string) => void;
waitForWordPick: () => void;
pickWord: (words: string[]) => void;
startGuessing: () => void;
startDrawing: () => void;
chatMessage: (author: string, message: string) => void;
gameOver: (result: GameOver) => void;
}
export interface GameOver {
leaderboard: (Player & { time: number })[];
drawer: string;
word: string;
}
type Result<T> = { error: false; data: T } | { error: true; message: string };
export interface ClientToServerEvents {
joinGame: (id: string, username: string, callback: (result: Result<{id: string, isHost: boolean, players: Player[]}>) => void) => void;
startGame: () => void;
pickWord: (word: string) => void;
guessWord: (word: string, callback: (success: boolean) => void) => void;
}
export interface SocketData {
gameId: string;
}
export interface Player {
username: string;
isHost: boolean;
}
export type GameState = 'waiting' | 'selectingWord' | 'waitingForWord' | 'drawing' | 'guessing' | 'guessedCorrectly' | 'gameOver';
export interface ChatMessage {
author: string;
message: string;
}

27
src/lib/util.ts Normal file
View file

@ -0,0 +1,27 @@
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// https://www.tutorialspoint.com/levenshtein-distance-in-javascript
export function levenshteinDistance(a: string, b: string): number {
const track: number[][] = Array(b.length + 1)
.fill(null)
.map(() => Array(a.length + 1).fill(null));
for (let i = 0; i <= a.length; i += 1) {
track[0][i] = i;
}
for (let j = 0; j <= b.length; j += 1) {
track[j][0] = j;
}
for (let j = 1; j <= b.length; j += 1) {
for (let i = 1; i <= a.length; i += 1) {
const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
track[j][i] = Math.min(
track[j][i - 1] + 1, // deletion
track[j - 1][i] + 1, // insertion
track[j - 1][i - 1] + indicator // substitution
);
}
}
return track[b.length][a.length];
}

10
src/main.ts Normal file
View file

@ -0,0 +1,10 @@
import './app.css';
import App from './App.svelte';
import '@fontsource/noto-sans';
import 'bulma/css/bulma.css';
const app = new App({
target: document.getElementById('app'),
});
export default app;

147
src/server/game.ts Normal file
View file

@ -0,0 +1,147 @@
import type { Server, Socket } from "socket.io";
import type { ClientToServerEvents, Player, ServerToClientEvents, SocketData } from "src/lib/socket";
import { levenshteinDistance } from "../lib/util.js";
import randomWords from "./words.js";
export class Game {
io: Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>;
gameId: string;
players: Map<string, Player>;
host: string;
startTime: number | null;
currentPlayer: string | null;
currentWord: string | null;
successfulPlayers: [string, number][];
constructor(gameId: string, io: Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>) {
this.io = io;
this.gameId = gameId;
this.players = new Map();
}
playerJoin(socket: Socket<ClientToServerEvents, ServerToClientEvents, {}, SocketData>, username: string): string | Player {
for (const player of this.players.values()) {
if (player.username === username) {
return 'username taken';
}
}
const isHost = this.players.size === 0;
if (isHost) {
this.host = socket.id;
}
const player: Player = {
isHost,
username,
};
this.players.set(socket.id, player);
return player;
}
/**
* Called when a client socket disconnects.
*
* @param socket Socket that has just disconnected
* @returns true if the game is empty, false otherwise
*/
playerLeft(socket: Socket<ClientToServerEvents, ServerToClientEvents, {}, SocketData>): boolean {
const player = this.players.get(socket.id);
this.players.delete(socket.id);
if (this.players.size === 0) return true;
if (player.isHost) {
// we need to assign a new host for this room
const playerIds = Array.from(this.players.keys());
const newHostId = playerIds[Math.floor(Math.random() * playerIds.length)];
const newHost = this.players.get(newHostId);
newHost.isHost = true;
this.io.to(this.gameId).emit('becameHost', newHost.username);
this.host = newHostId;
}
this.io.to(this.gameId).emit('playerLeft', player.username);
return false;
}
/**
* Called when the host presses the 'Start game' button.
* Will pick a random player to choose a word, and then
* broadcast the start to the other players.
*/
gameStart(socket: Socket<ClientToServerEvents, ServerToClientEvents, {}, SocketData>) {
if (!this.players.get(socket.id).isHost) return;
const playerIds = Array.from(this.players.keys());
const player = playerIds[Math.floor(Math.random() * playerIds.length)];
const words = randomWords(3);
this.currentPlayer = player;
this.successfulPlayers = [];
this.io.to(this.gameId).except(player).emit('waitForWordPick');
this.io.to(player).emit('pickWord', words);
}
pickWord(socket: Socket<ClientToServerEvents, ServerToClientEvents, {}, SocketData>, word: string) {
if (socket.id !== this.currentPlayer) return;
this.currentWord = word;
this.io.to(this.gameId).except(socket.id).emit('startGuessing');
this.io.to(socket.id).emit('startDrawing');
this.startTime = new Date().getTime();
}
/**
* Called when a player makes a guess during a game.
*
* @param socket The socket of the player that made the guess
* @param guess The word that the player guessed
* @returns true if the word was correct, false otherwise
*/
guessWord(socket: Socket<ClientToServerEvents, ServerToClientEvents, {}, SocketData>, guess: string): boolean {
if (!this.currentWord || this.successfulPlayers.find(([id, _]) => id === socket.id) !== undefined) return;
const player = this.players.get(socket.id);
if (guess.toLocaleLowerCase() === this.currentWord.toLocaleLowerCase()) {
// Correct answer!
this.io.to(this.gameId).except(socket.id).emit('chatMessage', 'game', `${player.username} guessed the correct word`);
this.successfulPlayers.push([socket.id, new Date().getTime() - this.startTime]);
return true;
} else if (levenshteinDistance(guess.toLocaleLowerCase(), this.currentWord.toLocaleLowerCase()) <= 2) {
// close guess
this.io.to(socket.id).emit('chatMessage', 'game', `${guess} is close!`);
}
// Broadcast incorrect guesses to all players
this.io.to(this.gameId).emit('chatMessage', player.username, guess);
return false;
}
checkGameOver() {
if (this.isGameOver()) {
const leaderboard = this.successfulPlayers.map(([id, time]) => {
const p = this.players.get(id);
return {
...p,
time,
};
});
this.io.to(this.gameId).emit('gameOver', {
leaderboard,
drawer: this.players.get(this.currentPlayer).username,
word: this.currentWord,
});
}
}
private isGameOver(): boolean {
for (const id of this.players.keys()) {
if (id !== this.currentPlayer && this.successfulPlayers.find(([i, _]) => id === i) === undefined) {
return false;
}
}
return true;
}
}

109
src/server/main.ts Normal file
View file

@ -0,0 +1,109 @@
import { Server } from 'socket.io';
import type {
ClientToServerEvents,
Player,
ServerToClientEvents,
SocketData,
} from 'src/lib/socket';
import { Game } from './game.js';
import randomWords from './words.js';
const io = new Server<
ClientToServerEvents,
ServerToClientEvents,
{},
SocketData
>({
cors: {
origin: true,
},
});
const games = new Map<string, Game>();
io.on('connection', (socket) => {
socket.on('joinGame', (gameId, username, callback) => {
if (gameId.length < 1 || gameId.length > 32) {
callback({
error: true,
message: 'invalid game id',
});
return;
}
if (username.length === 0 || !/^[a-zA-Z0-9]+$/.test(username)) {
callback({
error: true,
message: 'invalid username',
});
return;
}
if (!games.has(gameId)) {
games.set(gameId, new Game(gameId, io));
}
const room = games.get(gameId);
const player = room.playerJoin(socket, username);
if (typeof player === 'string') {
return callback({
error: true,
message: player,
});
}
io.to(gameId).emit('playerJoined', username);
socket.data.gameId = gameId;
socket.rooms.forEach(room => room !== socket.id && socket.leave(room));
socket.join(gameId);
const players = Array.from(room.players.values());
players.sort((a, b) => a.username.localeCompare(b.username));
callback({
error: false,
data: {
id: gameId,
isHost: player.isHost,
players,
},
});
});
socket.on('startGame', () => {
if (!socket.data.gameId) return;
const game = games.get(socket.data.gameId);
game.gameStart(socket);
});
socket.on('pickWord', (word) => {
if (!socket.data.gameId) return;
const game = games.get(socket.data.gameId);
game.pickWord(socket, word);
});
socket.on('guessWord', (word, callback) => {
if (!socket.data.gameId) return;
const game = games.get(socket.data.gameId);
const result = game.guessWord(socket, word);
callback(result);
game.checkGameOver();
});
socket.on('disconnecting', () => {
socket.leave(socket.id);
if (socket.data.gameId) {
if (games.get(socket.data.gameId).playerLeft(socket)) {
games.delete(socket.data.gameId);
}
}
});
});
console.log('listening on port :3000');
io.listen(3000);

14
src/server/words.ts Normal file
View file

@ -0,0 +1,14 @@
import words from '../assets/wordlist.json' assert {type: 'json'};
export default function randomWords(count = 3): string[] {
const randomWords: string[] = [];
while (randomWords.length < count) {
const word = words[Math.floor(Math.random() * words.length)];
if (!randomWords.includes(word)) {
randomWords.push(word);
}
}
return randomWords;
}

2
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

7
svelte.config.js Normal file
View file

@ -0,0 +1,7 @@
import sveltePreprocess from 'svelte-preprocess'
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
}

21
tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

8
tsconfig.node.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}

30
vite.config.ts Normal file
View file

@ -0,0 +1,30 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
svelte(),
VitePWA({
includeAssets: ['favicon.png', 'apple-touch-icon.png'],
manifest: {
name: 'Doodly',
short_name: 'Doodly',
description: 'Drawing game with friends',
theme_color: '#ffffff',
icons: [
{
src: 'favicon.png',
sizes: '256x256',
type: 'image/png',
},{
src: 'favicon-192.png',
sizes: '192x192',
type: 'image/png',
},
],
},
}),
],
});