Let's build a multiplayer movie trivia/quiz game with socket.io, svelte and node. devlog #6
Zoppatorsk
Posted on August 21, 2022
Pictures says more than thousand words they say..
So, guess just start with some pictures..
Start Screen for mobile
So u need to enter a name and an avtar seed
This is stored in local storage so will be remembered when visit page again.
Lobby for mobile
It needs some more css work later on.. for now good enough.
Regular lobby with countdown going for starting game
Question.
Just some hard coded questions in the backend for now.
Round Results
Showing the result of the round. DNA means did not answer, player did not answer before time ran out. Correct answer show a ✔ and incorrect ❌
The css is in no way finished.. for now it's just some quick throw-together so can display something more fun that just text... ;)
Code
So in the last log, I mentioned I had implemented the schematics I made earlier except the handling of disconnects during various states of the game. This is now fixed.
I simply extracted the needed code from the handler into a function and depending on the game state when a player disconnect the appropriate function is run.
Handler just call the function
//use disconnecting instead of discconnect so still have access to room of socket n stuff
socket.on('disconnecting', () => {
disconnecting(io, socket, games);
});
And here is function
function disconnecting(io, socket, games) {
//check if player is in a game and if so remove them from the game..
if (socket.rooms.size > 1) {
for (const room of socket.rooms) {
//seems stuff can be undefined for some time so added a check for this too.
if (room !== socket.id && games.get(room) !== undefined) {
const game = games.get(room);
game?.leave(socket.id);
//delete room if empty
if (game?.players.size === 0) games.delete(room);
else {
//notify the other players that the player has left the game
io.to(game.id).emit('player-left', socket.id);
//-----chek the state of the game and run needed function
switch (game.status) {
case 'waiting-for-start':
shouldGameStart(io, game);
break;
case 'waiting-for-ready':
shouldStartRound(io, game);
break;
case 'waiting-for-answer':
shouldEndRound(io, game);
break;
}
}
break;
}
}
}
}
For now no real generation of questions. Just some hard coded ones for testing.
Later will need figure out a common structure for questions.
module.exports = async function generateQuestions(no) {
const questions = [
{ question: 'What is the capital of the United States?', answers: ['Washington', 'New York', 'Los Angeles'], correctAnswer: 'Washington', type: 'pick-one' },
{ question: 'What is the capital of the United Kingdom?', answers: ['London', 'Manchester', 'Liverpool'], correctAnswer: 'London', type: 'pick-one' },
{ question: 'What is the capital of the Sweden?', answers: ['Stockholm', 'Oslo', 'Madrid'], correctAnswer: 'Stockholm', type: 'pick-one' },
];
//select no random questions from the array
const selectedQuestions = [];
for (let i = 0; i < no; i++) {
const randomIndex = Math.floor(Math.random() * questions.length);
selectedQuestions.push(questions[randomIndex]);
}
console.log('q', selectedQuestions);
return selectedQuestions;
};
The Svelte components are still just rather simple.
Start.svelte.. what is shown first, here u choose name n avatar n what u want to do, like create or join game
<script>
import { onMount } from 'svelte';
import { activeComponent, players, gameProps, playerId } from '../lib/stores';
export let socket;
let playername = '';
let seed = '';
//get player name and seed from localstorage
onMount(() => {
let player = localStorage.getItem('player');
if (player !== null) {
player = JSON.parse(player);
// @ts-ignore
playername = player?.name || ''; //these safeguards are really not needed i guess
// @ts-ignore
seed = player?.seed || '';
}
});
//set player name and seed to localstorage when it chages
$: {
if (playername || seed) localStorage.setItem('player', JSON.stringify({ name: playername, seed: seed }));
}
function createGame() {
let data = { name: playername, avatar: seed };
socket.emit('create-game', data, (response) => {
console.log(response.status);
if (response.status === 'ok') {
//set all the other response data in store.. playerId and gameData
players.set(response.players);
gameProps.set(response.gameData);
playerId.set(response.playerId);
//move to lobby
activeComponent.set('lobby');
}
});
}
/// if find open game will join it
function quickPlay() {
socket.emit('quick-play', (response) => {
if (response.gameId) {
socket.emit('join-game', { gameId: response.gameId, name: playername, avatar: seed }, (response) => {
if (response.status === 'ok') {
//set all the other response data in store.. playerId and gameData
players.set(response.players);
gameProps.set(response.gameData);
playerId.set(response.playerId);
//move to lobby
activeComponent.set('lobby');
}
});
} else alert('no game found');
});
}
function test() {
socket.emit('test');
}
</script>
<div class="wrapper">
<h1>Let's Play!</h1>
<img src={`https://avatars.dicebear.com/api/avataaars/:${seed}.svg`} alt="avatar" />
<input type="text" placeholder="Enter name" bind:value={playername} />
<input type="text" placeholder="Avatar Seed" bind:value={seed} />
<div class="buttons" class:disabled={!seed || !playername}>
<button on:click={createGame}>Create Game</button>
<button on:click={quickPlay}>Quickplay</button>
<button on:click={test}>List Games</button>
</div>
</div>
Lobby.svelte .. here is where u end up after creating or joining game
<script>
import { players, gameProps } from '../lib/stores';
export let socket;
export let currentCount;
let clicked = false;
function playerReady() {
clicked = true;
socket.emit('player-ready', $gameProps.id);
}
</script>
<div class="component-wrapper">
<h1>Lobby</h1>
<h2>{$gameProps.id}</h2>
<div class="player-wrapper">
{#each $players as player}
<div>
<img src={player.avatar} alt="avatar" />
<p>{player.name}</p>
</div>
{/each}
</div>
<button on:click|once={playerReady} disabled={clicked}>Ready</button>
<progress value={currentCount} max={$gameProps.waitBetweenRound} />
<h2>{currentCount > -1 ? currentCount : 'Waiting'}</h2>
</div>
The question component is not even worth posting, it just generates buttons with the different possible answers n emit to server on press.
RoundResult.svelte will just display the results of the round.
<script>
export let currentCount;
export let roundResults;
import { players } from '../lib/stores';
</script>
<h2>Round Results</h2>
<div class="player-wrapper">
{#each $players as player}
<div>
<img src={player.avatar} alt="avatar" />
<p>{player.name}</p>
<p>{roundResults.find((x) => x.id == player.id).answer} <span>{roundResults.find((x) => x.id == player.id).answerCorrect ? '✔' : '❌'} </span></p>
</div>
{/each}
</div>
{#if currentCount > 0}
<h2>{currentCount}</h2>
{/if}
Ending notes
To generate the avatars I use https://avatars.dicebear.com. Awesome stuff!
Pulled in pico.css so I don't have to mock around and style everything myself, saves time.
Now just need to keep implement the missing things. Score system, end game results, question generator, question components and soo on.. For now I think it's good though, all logic seems to be working as intended.
Over n out.. now chill n watch some Korean series..
Posted on August 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 21, 2022