DEV Community

Zoppatorsk
Zoppatorsk

Posted on

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

“Lift off, we have a lift off!”

So yesterday I made a plan, showing the flow of events or whatever..

Today I implemented it, or well still have not implement the handling of players that disconnects mid game, but that will be next.
Things went smooth overall. :) Just look at the flowchart thing and write code that implements it.

I now have a first working prototype for the game loop logic from start of game until end.

let's have a look at what i did. We start with the Game class.

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

module.exports = class Game {
    constructor({ maxPlayers = 5, rounds = 2 } = {}) {
        this.id = nanoid();
        this.maxPlayers = maxPlayers;
        this.rounds = rounds;
        this.round = 1;
        this.waitBetweenRounds = 5;
        this.roundTime = 30;
        this.status = 'open';
        this.players = new Map();
        this.roundCountDown = null; //will hold the interval timer for the round
        this.answers = { 1: {}, 2: {}, 3: {} }; //for now just store answers here in hardcoded way, probably wld be better if stored in player object.
    }

    startRoundCountDown(io, func) {
        let count = this.roundTime + 1;
        this.roundCountDown = setInterval(() => {
            count--;
            io.to(this.id).emit('count-down', count);
            if (count === 0) {
                this.clearRoundCountDown();
                func(io, this);
            }
        }, 1000);
    }

    clearRoundCountDown() {
        clearInterval(this.roundCountDown);
    }

    join(player) {
        //check if plyer 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);
    }

    resetPlayerReady() {
        this.players.forEach((player) => {
            player.ready = false;
        });
    }
    howManyPlayersReady() {
        let ready = 0;
        this.players.forEach((player) => {
            if (player.ready) ready++;
        });
        return ready;
    }
    allPlayersHaveAnswered() {
        let noAnswers = 0;
        this.players.forEach((player) => {
            if (this.answers?.[this.round]?.[player.id] !== undefined) {
                noAnswers++;
            }
        });
        return noAnswers === this.players.size;
    }

    getPublicData() {
        return {
            id: this.id,
            round: this.round,
            rounds: this.rounds,
            status: this.status,
        };
    }

    //easier to do stuff on frontend with players as an array instead of a map
    getPlayersAsArray() {
        let playersArr = [];
        //convert the players map to an array.. this could probably be done cleaner and in one line but I am not used to working with maps
        //this will probably be overhauled later
        this.players.forEach((player) => {
            playersArr.push({ ...player });
        });
        return playersArr;
    }

    compileResults() {
        //later use this to compile the results of the game
        return {};
    }
};
Enter fullscreen mode Exit fullscreen mode

I have added some properties, the most important one is the roundCountDown.This prop will hold an interval timer for counting down the round. The reason I put it on the class is that it needs to be tied to an instance of the game and i need to be able to start and clear it from different places in the event handling code.

Let's have a closer look at the method

startRoundCountDown(io, func) {
        let count = this.roundTime + 1;
        this.roundCountDown = setInterval(() => {
            count--;
            io.to(this.id).emit('count-down', count);
            if (count === 0) {
                this.clearRoundCountDown();
                func(io, this);
            }
        }, 1000);
    }
Enter fullscreen mode Exit fullscreen mode

it takes in io and a function, the function it takes is the function that needs to run when either the time is up or all players have submitted their answers. This function needs 2 arguments, io so it can emit events (this is already available as it's been passed into the method) and the other one is the game, here "this" is the game so that's handy.

Ofc the this will only run if time is up before all players have answered. If all players have answered before the interval will be stopped and remove. The other code that can trigger the function is in the eventHandler.

Below u can see the function that is run.. this function ofc lives outside the Game class.

function endRound(io, game) {
    game.round++;
    if (game.round > game.rounds) {
        game.status = 'end-game';
        io.to(game.id).emit('end-game', game.compileResults());
        games.delete(game.id);
    } else {
        game.status = 'end-round';
        io.to(game.id).emit('end-round'); //need to send with some reuslts later
        getReady(io, game);
    }
}
Enter fullscreen mode Exit fullscreen mode

Below we have the code that runs the game..
I have omitted the stuff for create game, join game n soo on..

So when a player in the lobby is ready to start the game a 'player-ready' event is sent

        socket.on('player-ready', (gameId) => {
            const game = games.get(gameId);

            //maybe we need to do something here later except reurn but probably not, this is a safeguard if socket reconnects n start sending shit when game is in another state
            if (game.status !== 'open' && game.status !== 'waiting-for-start') return;

            //when player is ready shld.. change the ready variable of player
            game.players.get(socket.id).ready = true;
            if (game.status !== 'waiting-for-start') game.status = 'waiting-for-start'; //now we do not accept any new players

            //if half of players are not ready then just return
            if (game.howManyPlayersReady() < game.players.size / 2) return;
            //here shld run a function that is reused everytime a new round starts
            getReady(io, game);
        });

Enter fullscreen mode Exit fullscreen mode

As u can see the last thing that happens is running the getReady function.
This will start a countdown for the game to start and emit 'ready-round' when done.

This code will also run after each round is finished and count in the new round.

function getReady(io, game) {
    game.status = 'get-ready';
    game.resetPlayerReady();
    let count = game.waitBetweenRounds + 1;
    const counter = setInterval(countdown, 1000, game.id);

    function countdown(gameId) {
        count--;
        console.log(count);
        io.to(gameId).emit('count-down', count);
        if (count == 0) {
            clearInterval(counter);
            io.to(gameId).emit('ready-round'); //here neeed to send with some junk later.. like question n metadata about it
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Next that happens is we wait for all the player clients will acknowledge that they are ready. They do so by sending a 'player-ready-round' event

It is handled in the code below. When got ready from all players
'round-start' is emitted and the countdown interval I wrote about in the beginning is started.

        socket.on('player-ready-round', (gameId) => {
            const game = games.get(gameId);
            if (game.status !== 'get-ready' && game.status !== 'waiting-for-ready') return;
            if (game.status !== 'waiting-for-ready') game.status = 'waiting-for-ready';
            game.players.get(socket.id).ready = true;
            if (game.howManyPlayersReady() !== game.players.size) return;
            game.status = 'waiting-for-answer';
            io.to(gameId).emit('round-start');
            game.startRoundCountDown(io, endRound);
        });
Enter fullscreen mode Exit fullscreen mode

Now we just wait for all players to answer or for the time to be up until we finish the round (the same endRound() function as i posted a bit longer up). This endRound function will determine if should just end this round by emitting 'end-round' and ready the next round (same getReady function as before) or end the game by emitting 'end-game'.

socket.on('answer', (gameId, answer) => {
            const game = games.get(gameId);
            if (game.status !== 'waiting-for-answer') return;
            //store the answer.. for now it's stored in the game object as an object
            game.answers[game.round][socket.id] = answer;
            //check if all players have answered
            if (game.allPlayersHaveAnswered() == false) return;
            //clear the interval for counting down as we now ends the round as all players have answered
            game.clearRoundCountDown();
            //run endRound logic
            endRound(io, game);
        });
Enter fullscreen mode Exit fullscreen mode

And yeah, that's like all it's too it.. good thing I made that chart, right!

The frontend code is soo simple now it's like not even worth showing, but here it comes.

socket.on('count-down', (count) => {
        currentCount = count;
    });

    socket.on('ready-round', () => {
        socket.emit('player-ready-round', $gameProps.id);
    });

    socket.on('round-start', () => {
        $activeComponent = 'question';
    });

    socket.on('end-round', () => {
        $activeComponent = 'roundresult';
    });

    socket.on('end-game', () => {
        $activeComponent = 'gameresult';
    });
Enter fullscreen mode Exit fullscreen mode

Most of it just change a store for what component should be shown
All countdowns is handled by the 'count-down' listener and it only sets a variable to the value, this variable is passed down to the components that need it.

Later I might change this to a store variable instead, that way I should be able to extract all the socket logic into it's own regular Javascript file. But will see about it, might make sense to keep it in the Svelte component as there will be more data passed later, like results of round and game and the question.

The next thing will be to break down some of the event handlers on the server a bit more so can handle things if players leave mid-game.

After that it's time to keep working on making this thing into an actual game that can be played.

Top comments (1)

Collapse
 
koyiy profile image
Zanar

Hello. Some users who have no experience of playing in casinos experience problems with registration at ComicPlay. However, I recommend that you visit this site to solve this problem. After all, it offers detailed and clear instructions on how to register an account at this casino. After analysing this, you will be able to create an account very quickly and smoothly.