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

zoppatorsk

Zoppatorsk

Posted on August 18, 2022

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

In the last log I got the basic stuff setup and the socket communication up and working.

Now I have done some actual planning.
Behold, a simple flowchart
flowchart

Ranting

Before getting into the code stuff I have been working on just need to rant a bit..

My brain do not like coding event driven stuff.. many times I just can't make sense of it... last time I socket.io to make something a bit more complex I had to rewrite it all at least 3 times and it was still jank.. working, but jank.. let's hope this project will go better.

Rant over, let's do another dive into code without some actual planning, except the flowchart but that one does not care about the event driven stuff.. ;)

Backend codes

So I'm not a fan of OOP.. but sometimes creating some classes and objects just make sense.

The game needs players so I created a Player class. For now it's simple and later will for sure put more stuff in it.
The id used is just the socket.id, makes it ez to keep track of things.

const { nanoid } = require('nanoid'); //temp dependecy so can generate users with uniqe names without me having to do any typing.. lazy, yes..!!

//add more stuff to player class later when needed.. score maybe.. heeh...
module.exports = class Player {
    constructor({ name = 'John Doe' + nanoid(), id = null } = {}) {
        this.playername = name;
        this.id = id;
        this.ready = false;
    }
};

Enter fullscreen mode Exit fullscreen mode

So, we have a Player class to make player, now the player also need a game to be in.. so now create a Game class. (remember this is supposed to be multiplayer and can have several different games running at the same time.)

const { nanoid } = require('nanoid');

module.exports = class Game {
    constructor({ maxPlayers = 5, rounds = 3 } = {}) {
        this.id = nanoid();
        this.maxPlayers = maxPlayers;
        this.rounds = rounds;
        this.round = 1;
        this.status = 'open';
        this.players = new Map();
    }

    join(player) {
        //check if player is allowed to join
        if (this.status === 'open' && this.players.size < this.maxPlayers) {
            this.players.set(player.id, player);
            return true;
        }
        return false;
    }

    leave(playerid) {
        this.players.delete(playerid);
    }
};
Enter fullscreen mode Exit fullscreen mode

So now players be in a game and they can join and leave the game.
Just using a map to store the players in.

Now it's time to implement some actual logic.. so back to the eventHandler.js file where all the socket.io stuff happens..

const Game = require('./Game');
const Player = require('./Player');

module.exports = function (io) {
    const games = new Map(); //use a map to store all the games so can easily access them by id

    io.on('connection', function (socket) {
        const count = io.engine.clientsCount; 
        console.log(socket.id + ' connected c:' + count); //just for debugging purposes

        socket.on('disconnecting', () => {
            //check if player is in a game and if so remove them from the game..
            //so we check if size of rooms are larger than 1 (each socket is in at least one room, it's own, if it's in 2 then it means it is in a game)
            if (socket.rooms.size > 1) {
                for (const room of socket.rooms) {
                    if (room !== socket.id) {
                        games.get(room).leave(socket.id);

                        //delete room if empty
                        if (games.get(room).players.size === 0) games.delete(room);
                        else {
                            //notify the other players that the player has left the game
                            //chek the state of the game and finish round if all other playeres have asnwered
                        }
                        break;
                    }
                }
            }
            console.log(socket.id + ' disconnected');
        });

        //when player have selected his settings and game should be created.. data should hold the settings, just omitt for now and run on default settings
        socket.on('create-game', function (data, callback) {
            console.log('create-game');

            //create the game
            const game = new Game();

            //store the id
            const gameid = game.id;

            //create the player.. later add junk like name n such.
            const player = new Player({ id: socket.id });

            //add the player to the game
            game.join(player);

            //store the game in the games map
            games.set(game.id, game);

            //join the socket into a room for the game.. roomname is same as gameid
            socket.join(gameid);

            //-----here we should create the questions that the game will use

            //callback to the client that the game has been created
            //this should take the player to the lobby.
            callback({ status: 'ok' });
        });

        //when a player want to joins a game
        socket.on('join-game', function (data, callback) {
            console.log('join-game');
            //data shld be like { player: { name: '', etc.. }, gameid: '' }

            //check the game status n stuff so it is ok to join
            const game = games.get(data.gameid);

            //create player
            const player = new Player({ id: socket.id });
            //try to join the game
            const successfulJoin = game.join(player);
            //if ok then join socket room
            if (successfulJoin) {
                socket.join(data.gameid);
                callback({ status: 'ok' });
                //this should take the player to the lobby...
                //maby I will need to run with emitts instead of callback !!??
                //Client will need some info about the game.. (room id n stuff I guess)
            } else {
                //can add reason later if want..
                callback({ status: 'failed' });
                //this should take the player back to start screen or serverlist?... maybe add something in data later so can tell if player came from quickstart or serverlist
            }
        });

        //just a testing function so can check on various thins
        socket.on('test', () => {
            console.log(games);
        });
    });

    //should this be in connection?? or is it ok to have it here?.. I dont know when it triggers.. check on later
    io.engine.on('connection_error', (err) => {
        console.log('CONNECTION_ERROR!!');
        console.log(err.req); // the request object
        console.log(err.code); // the error code, for example 1
        console.log(err.message); // the error message, for example "Session ID unknown"
        console.log(err.context); // some additional error context
    });
};
Enter fullscreen mode Exit fullscreen mode

So now there is a games map, this will store all the running games.

When a client emit "create-game" (when the press a create game button or something) a game is create from the Game class, for now only using the default settings from the class.
Then player is create and joined to the game. When this is done the socket is also joined to "room" with the same id as the game.

For other ppl to be able to join they will have to emit 'join-game'. Some checks are done and if all is good the player is joined to the game and also to the "socket.io room"

Booth of these "event listeners" will also run a callback when done, this is so they can notify the client of stuff it needs to know. I'm not sure if it is the correct approach to take, but to me it seems the cleanest way of doing it. The other option wld be to emit back event to the client but for that wld need to set up more listeners and stuff wld probably get rather messy fast.

I have also been setting up the 'disconnecting' listener that will run when a client (socket) disconnects, this happens for example if close the browser on the client.
It is basically just doing clean up. Removing the associated player from the game and delete the game if it has no players.

Client code

Well, not much is happening on client yet.. I created two simple components.. "Start" that is shown when you start the client, ie, go to the webpage.

Start.svelte

<script>
    import { activeComponent } from '../lib/stores';
    export let socket;

    function createGame() {
        let data = { name: 'test' };
        socket.emit('create-game', data, (response) => {
            console.log(response.status);
            if (response.status === 'ok') {
                activeComponent.set('lobby');
            }
        });
    }

    function quickPlay() {
        //implement later
    }

    function test() {
        socket.emit('test');
    }
</script>

<div>
    <button on:click={createGame}>Create Game</button>
    <button on:click={quickPlay}>Quickplay</button>
    <button on:click={test}>List Games</button>
</div>

<style>
    button {
        color: white;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

So yeah, it only has 3 buttons, pressing create game will create a game like i talked about in the backend section.
If get an 'ok' repsonse from the callback we set the store activeComponent to 'lobby' .. this will remove this component and show the lobby component.. this done by a "Svelte if" in the App.svelte, so yeah, let's look at that one next.

App.svelte

<script>
    import { io } from 'socket.io-client';
    import { activeComponent } from './lib/stores/';
    import Start from './components/Start.svelte';
    import Lobby from './components/Lobby.svelte';

    let connected = '';
    //do we need to put stuff in onmount?? guess will find out later..

    const socket = io('http://localhost:3000');

    socket.on('connect', () => {
        connected = 'We got a signal!';
    });
</script>

<main>
    <h1>{connected}</h1>
    {#if $activeComponent === 'start'}
        <Start {socket} />
    {/if}
    {#if $activeComponent === 'lobby'}
        <Lobby {socket} />
    {/if}
</main>

<style>
</style>
Enter fullscreen mode Exit fullscreen mode

As u can see there is not much going on yet.. just switching what component should be shown.

The Lobby.svelte is even more simple, it has no logic yet.

<script>
    export let socket;
</script>

<h1>Lobby</h1>
Enter fullscreen mode Exit fullscreen mode

So at this point I can create a game by pressing the button and after that will be "transported" to the lobby.

Soo far it's all about setting up the basic logic and get things to work the correct way... code for actual "playing the game" will come later..

That's all for now.. like, subscribe and hit the notification bell.. haha.. or not... guess watched too much youtube ;)

If find any spelling mistakes or stuff that makes no sense, yeah, then it's on me.. I don't proof read these logs and types what comes to mind.

💖 💪 🙅 🚩
zoppatorsk
Zoppatorsk

Posted on August 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related