Let's build a multiplayer movie trivia/quiz game with socket.io, svelte and node. devlog #6

zoppatorsk

Zoppatorsk

Posted on August 21, 2022

Let's build a multiplayer movie trivia/quiz game with socket.io, svelte and node. devlog #6

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.
start_screen_mobile

Lobby for mobile

It needs some more css work later on.. for now good enough.
lobby_mobile

Regular lobby with countdown going for starting game

regular_lobby

Question.

Just some hard coded questions in the backend for now.
question

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 ❌
round_result

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); 
        });
Enter fullscreen mode Exit fullscreen mode

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;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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..

💖 💪 🙅 🚩
zoppatorsk
Zoppatorsk

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