Pojďme si vytvořit filmovou kvízovou hru pro více hráčů se socket.io, svelte a node. vývojář #5

"Vystupte, máme odvoz!"

Takže včera jsem udělal plán, ukazující tok událostí nebo cokoli jiného..

Dnes jsem to implementoval, nebo ještě neimplementoval manipulaci s hráči, která odpojuje střední hru, ale to bude příště.
Věci šly celkově hladce. :) Stačí se podívat na vývojový diagram a napsat kód, který to implementuje.

Nyní mám první funkční prototyp logiky herní smyčky od začátku hry do konce.

pojďme se podívat na to, co jsem udělal. Začneme třídou Game.

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

Přidal jsem několik vlastností, z nichž nejdůležitější je roundCountDown. Tato rekvizita bude obsahovat intervalový časovač pro odpočítávání kola. Důvod, proč jsem to dal do třídy, je ten, že musí být svázán s instancí hry a musím být schopen ji spustit a vymazat z různých míst v kódu zpracování událostí.

Pojďme se na metodu podívat blíže

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

zabírá io a funkci, funkce, kterou potřebuje, je funkce, která musí být spuštěna, když vypršel čas nebo všichni hráči odeslali své odpovědi. Tato funkce potřebuje 2 argumenty, io, aby mohla vydávat události (to je již k dispozici, protože to bylo předáno do metody) a druhý je hra, zde "toto" je hra, takže se to hodí.

Toto se spustí pouze tehdy, pokud uplyne čas, než všichni hráči odpověděli. Pokud všichni hráči odpověděli dříve, interval bude zastaven a odstraněn. Další kód, který může funkci spustit, je v eventHandler.

Níže u vidíte funkci, která je spuštěna.. tato funkce ofc žije mimo třídu Game.

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

Níže máme kód, který spouští hru..
Vynechal jsem věci pro vytvoření hry, připojte se ke hře atd..

Když je tedy hráč v lobby připraven ke spuštění hry, odešle se událost „připraven pro hráče“.

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

Jak vidíte, poslední věc, která se stane, je spuštění funkce getReady.
Tím se zahájí odpočítávání pro začátek hry a po dokončení se spustí 'přípravné kolo'.

Tento kód se také spustí po dokončení každého kola a započítá se do nového kola.

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

Dále čekáme, až všichni klienti hráčů potvrdí, že jsou připraveni. Činí tak odesláním události „player-ready-round“

Je to řešeno v kódu níže. Když jsem byl připraven od všech hráčů
Vydá se 'round-start' a spustí se odpočítávací interval, o kterém jsem psal na začátku.

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

Nyní jen počkáme, až všichni hráči odpoví nebo než uplyne čas, dokud nedokončíme kolo (stejná funkce endRound() jako o něco déle). Tato funkce endRound určí, zda se má toto kolo ukončit vysláním „end-round“ a připravit se na další kolo (stejná funkce getReady jako předtím) nebo ukončit hru vydáním „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);
        });

A jo, to je jakoby všechno.. dobře, že jsem ten graf udělal, že jo!

Kód frontendu je nyní tak jednoduchý, jako by ani neměl cenu ukazovat, ale přichází.

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

Většina z toho jen změní obchod, který komponent by měl být zobrazen
Všechna odpočítávání zpracovává posluchač 'odpočítávání' a nastavuje pouze proměnnou na hodnotu, tato proměnná je předávána komponentám, které ji potřebují.

Později bych to mohl změnit na proměnnou úložiště, takže bych měl být schopen extrahovat veškerou logiku soketu do vlastního běžného souboru Javascript. Ale uvidíme, možná by mělo smysl ponechat to v komponentě Svelte, protože později bude předáno více dat, jako jsou výsledky kola a hry a otázka.

Další věcí bude trochu více rozebrat některé ovladače událostí na serveru, aby bylo možné zvládnout věci, pokud hráči odejdou uprostřed hry.

Poté je čas pokračovat v práci na tom, aby se z této věci stala skutečná hra, kterou lze hrát.


No