Source

lib/lobbyStateHandlers.js

const logger = require('./logger');
const CONSTANTS = require('./constants');
const Fp = require('./util/fp');
const equalsLong = require('./util/equalsLong');
const getRandomInt = require('./util/getRandomInt');
const SnowflakeUtil = require('discord.js/src/util/Snowflake');

const LobbyStateTransitions = {
    [CONSTANTS.STATE_CHECKING_READY]: {
        [CONSTANTS.QUEUE_TYPE_DRAFT]: CONSTANTS.STATE_SELECTION_PRIORITY,
        [CONSTANTS.QUEUE_TYPE_CHALLENGE]: CONSTANTS.STATE_SELECTION_PRIORITY,
        [CONSTANTS.QUEUE_TYPE_AUTO]: CONSTANTS.STATE_AUTOBALANCING,
    },
};

const lobbyStateNoOp = async _lobbyState => ({ ..._lobbyState });

/**
 * This provides methods used for ihlManager lobby state handling.
 * @mixin
 * @memberof module:ihlManager
 */
const LobbyStateHandlers = ({ DotaBot, Db, Guild, Lobby, MatchTracker }) => ({
    async [CONSTANTS.STATE_NEW](_lobbyState) {
        await Db.updateLobbyState(_lobbyState)(CONSTANTS.STATE_WAITING_FOR_QUEUE);
        return { ..._lobbyState, state: CONSTANTS.STATE_WAITING_FOR_QUEUE };
    },
    async [CONSTANTS.STATE_WAITING_FOR_QUEUE](_lobbyState) {
        const lobbyState = this[_lobbyState.queueType](_lobbyState);
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_BEGIN_READY](_lobbyState) {
        let lobbyState = { ..._lobbyState };
        lobbyState = await Fp.pipeP(
            Lobby.assignLobbyName,
            Lobby.assignGameMode,
        )(lobbyState);

        lobbyState.channel = await Guild.setChannelName(lobbyState.lobbyName)(lobbyState.channel);
        lobbyState.role = await Guild.setRoleName(lobbyState.lobbyName)(lobbyState.role);
        await Guild.setChannelViewable(lobbyState.inhouseState.guild)(false)(lobbyState.channel);
        await Guild.setChannelViewableForRole(lobbyState.inhouseState.adminRole)(true)(lobbyState.channel);
        await Guild.setChannelViewableForRole(lobbyState.role)(true)(lobbyState.channel);
        await Lobby.addRoleToPlayers(lobbyState);

        // destroy any accepted challenges for players
        await Lobby.mapPlayers(Db.destroyAllAcceptedChallengeForUser)(lobbyState);

        if (!lobbyState.readyCheckTime) {
            lobbyState.readyCheckTime = Date.now();
        }
        this.registerLobbyTimeout(lobbyState);
        lobbyState.state = CONSTANTS.STATE_CHECKING_READY;

        await Db.updateLobby(lobbyState);
        this.onCreateLobbyQueue(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_CHECKING_READY](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        const playersNotReady = await Lobby.getNotReadyPlayers()(lobbyState);
        logger.silly(`STATE_CHECKING_READY ${lobbyState.id} playersNotReady ${playersNotReady.length}`);
        logger.silly(`STATE_CHECKING_READY ${lobbyState.id} readyCheckTimeout ${lobbyState.inhouseState.readyCheckTimeout} elapsed ${Date.now() - lobbyState.readyCheckTime}`);
        if (playersNotReady.length === 0) {
            await Lobby.removeLobbyPlayersFromQueues(lobbyState);
            lobbyState.state = LobbyStateTransitions[lobbyState.state][lobbyState.queueType];
            this.unregisterLobbyTimeout(lobbyState);
            this[CONSTANTS.MSG_PLAYERS_READY](lobbyState);
        }
        else if (Lobby.isReadyCheckTimedOut(lobbyState)) {
            logger.silly(`STATE_CHECKING_READY ${lobbyState.id} isReadyCheckTimedOut true`);
            await Fp.allPromise(playersNotReady.map((player) => {
                logger.silly(`STATE_CHECKING_READY ${lobbyState.id} removing player not ready ${player.id}`);
                if (lobbyState.captain1UserId === player.id) {
                    lobbyState.captain1UserId = null;
                }
                if (lobbyState.captain2UserId === player.id) {
                    lobbyState.captain2UserId = null;
                }
                return Fp.allPromise([
                    Lobby.removeUserFromQueues(player),
                    Lobby.removePlayer(lobbyState)(player),
                ]);
            }));
            await Lobby.returnPlayersToQueue(lobbyState);
            this[CONSTANTS.MSG_READY_CHECK_FAILED](lobbyState, playersNotReady);
            lobbyState.state = CONSTANTS.STATE_WAITING_FOR_QUEUE;
            lobbyState.readyCheckTime = null;
        }
        else {
            logger.silly(`STATE_CHECKING_READY ${lobbyState.id} isReadyCheckTimedOut false`);
        }
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_ASSIGNING_CAPTAINS](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        if (lobbyState.captain1UserId == null || lobbyState.captain2UserId == null) {
            const [captain1, captain2] = await Lobby.assignCaptains(lobbyState);
            logger.silly(`lobby run assignCaptains captain1 ${!!captain1} captain2 ${!!captain2}`);
            lobbyState.captain1UserId = captain1 ? captain1.id : null;
            lobbyState.captain2UserId = captain2 ? captain2.id : null;
            if (captain1 && captain2) {
                lobbyState.state = CONSTANTS.STATE_SELECTION_PRIORITY;
                this[CONSTANTS.MSG_ASSIGNED_CAPTAINS](lobbyState);
            }
            else {
                lobbyState.state = CONSTANTS.STATE_AUTOBALANCING;
                this[CONSTANTS.MSG_AUTOBALANCING](lobbyState);
            }
        }
        else {
            logger.silly(`lobby run assignCaptains captains exist ${lobbyState.captain1UserId}, ${lobbyState.captain2UserId}`);
            lobbyState.state = CONSTANTS.STATE_SELECTION_PRIORITY;
            this[CONSTANTS.MSG_ASSIGNED_CAPTAINS](lobbyState);
        }
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_SELECTION_PRIORITY](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        await Lobby.setPlayerFaction(1)(lobbyState)(lobbyState.captain1UserId);
        await Lobby.setPlayerFaction(2)(lobbyState)(lobbyState.captain2UserId);
        if (!lobbyState.playerFirstPick) {
            if (!lobbyState.selectionPriority) {
                lobbyState.selectionPriority = getRandomInt(2) + 1;
                await Db.updateLobby(lobbyState);
            }
            const captain = await Db.findUserById(lobbyState[`captain${3 - lobbyState.selectionPriority}UserId`]);
            this[CONSTANTS.MSG_PLAYER_DRAFT_PRIORITY](lobbyState, captain);
        }
        else if (!lobbyState.firstPick && !lobbyState.radiantFaction) {
            const captain = await Db.findUserById(lobbyState[`captain${lobbyState.selectionPriority}UserId`]);
            this[CONSTANTS.MSG_SELECTION_PRIORITY](lobbyState, captain);
        }
        else if (!lobbyState.firstPick && lobbyState.radiantFaction) {
            const captain = await Db.findUserById(lobbyState[`captain${3 - lobbyState.selectionPriority}UserId`]);
            this[CONSTANTS.MSG_SELECTION_PICK](lobbyState, captain);
        }
        else if (lobbyState.firstPick && !lobbyState.radiantFaction) {
            const captain = await Db.findUserById(lobbyState[`captain${3 - lobbyState.selectionPriority}UserId`]);
            this[CONSTANTS.MSG_SELECTION_SIDE](lobbyState, captain);
        }
        else {
            lobbyState.state = CONSTANTS.STATE_DRAFTING_PLAYERS;
            await Db.updateLobby(lobbyState);
        }
        logger.silly(`lobby run STATE_SELECTION_PRIORITY selectionPriority ${lobbyState.selectionPriority} playerFirstPick ${lobbyState.playerFirstPick} first pick ${lobbyState.firstPick} radiantFaction ${lobbyState.radiantFaction}`);
        return lobbyState;
    },
    async [CONSTANTS.STATE_DRAFTING_PLAYERS](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        const faction = await Lobby.getDraftingFaction(lobbyState.inhouseState.draftOrder)(lobbyState);
        const captain = await Lobby.getFactionCaptain(lobbyState)(faction);
        logger.silly(`lobby run STATE_DRAFTING_PLAYERS ${faction} ${captain} ${lobbyState.playerFirstPick} ${lobbyState.firstPick} ${lobbyState.radiantFaction}`);
        if (captain) {
            this[CONSTANTS.MSG_DRAFT_TURN](lobbyState, captain);
        }
        else {
            lobbyState.state = CONSTANTS.STATE_TEAMS_SELECTED;
        }
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_AUTOBALANCING](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        const playersNoTeam = await Lobby.getNoFactionPlayers()(lobbyState);
        const unassignedCount = playersNoTeam.length;
        logger.silly(`Autobalancing unassigned count ${unassignedCount}`);
        if (unassignedCount) {
            logger.silly('Autobalancing...');
            const getPlayerRating = Lobby.getPlayerRatingFunction(lobbyState.inhouseState.matchmakingSystem);
            await Lobby.autoBalanceTeams(getPlayerRating)(lobbyState);
            logger.silly('Autobalancing... done');
        }
        lobbyState.firstPick = getRandomInt(2) + 1;
        lobbyState.radiantFaction = getRandomInt(2) + 1;
        lobbyState.state = CONSTANTS.STATE_TEAMS_SELECTED;
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_TEAMS_SELECTED](_lobbyState) {
        const lobbyState = { ..._lobbyState, state: CONSTANTS.STATE_WAITING_FOR_BOT };
        this[CONSTANTS.MSG_TEAMS_SELECTED](lobbyState);
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_WAITING_FOR_BOT](_lobbyState) {
        let lobbyState = { ..._lobbyState };
        logger.silly(`STATE_WAITING_FOR_BOT ${lobbyState.id} ${lobbyState.botId}`);
        if (lobbyState.botId == null) {
            const bot = await Db.findUnassignedBot(lobbyState.inhouseState);
            if (bot) {
                logger.silly(`lobby run findUnassignedBot ${bot.steamId64}`);
                lobbyState.state = CONSTANTS.STATE_BOT_ASSIGNED;
                lobbyState = await Lobby.assignBotToLobby(lobbyState)(bot.id);
                this[CONSTANTS.MSG_BOT_ASSIGNED](lobbyState, bot);
            }
        }
        else {
            lobbyState.state = CONSTANTS.STATE_BOT_ASSIGNED;
        }
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_BOT_ASSIGNED](_lobbyState) {
        let lobbyState = { ..._lobbyState };
        if (lobbyState.botId != null) {
            const dotaBot = await this.loadBotById(lobbyState.botId);
            if (dotaBot) {
                // check if bot is in another lobby already
                if (!dotaBot.dotaLobbyId || equalsLong(lobbyState.dotaLobbyId, dotaBot.dotaLobbyId)) {
                    const tickets = await DotaBot.loadDotaBotTickets(dotaBot);
                    // check if bot has ticket if needed
                    if (!lobbyState.inhouseState.leagueid || tickets.find(ticket => ticket.leagueid === lobbyState.inhouseState.leagueid)) {
                        const lobbyOptions = { ...lobbyState, leagueid: lobbyState.inhouseState.leagueid };
                        const enterLobbyP = lobbyState.dotaLobbyId ? DotaBot.joinDotaBotLobby(lobbyOptions)(dotaBot) : DotaBot.createDotaBotLobby(lobbyOptions)(dotaBot);
                        enterLobbyP.then((inLobby) => {
                            if (inLobby) {
                                lobbyState.dotaLobbyId = dotaBot.dotaLobbyId.isZero() ? null : dotaBot.dotaLobbyId.toString();
                                lobbyState.state = CONSTANTS.STATE_BOT_STARTED;
                                return Db.updateLobby(lobbyState).then(() => {
                                    logger.silly(`updateLobby done. ${lobbyState.id} ${lobbyState.state}`);
                                    logger.silly(`STATE_BOT_ASSIGNED to STATE_BOT_STARTED ${lobbyState.id} ${lobbyState.state}`);
                                    this[CONSTANTS.EVENT_RUN_LOBBY](lobbyState, [CONSTANTS.STATE_BOT_STARTED]).catch(e => logger.error(e));
                                });
                            }

                            lobbyState.state = CONSTANTS.STATE_WAITING_FOR_BOT;
                            return Lobby.unassignBotFromLobby(lobbyState).then((__lobbyState) => {
                                this[CONSTANTS.MSG_BOT_UNASSIGNED](__lobbyState, 'Failed to enter lobby.');
                            });
                        }).catch((err) => {
                            logger.error(err);
                            this.removeBot(lobbyState.botId);
                            lobbyState.state = CONSTANTS.STATE_BOT_FAILED;
                            return Db.updateLobby(lobbyState).then(() => Lobby.unassignBotFromLobby(lobbyState))
                                .then((lobbyStateBotless) => {
                                    this[CONSTANTS.MSG_BOT_UNASSIGNED](lobbyStateBotless, 'Failed to create lobby.');
                                    this[CONSTANTS.EVENT_RUN_LOBBY](lobbyStateBotless, [CONSTANTS.STATE_BOT_FAILED]).catch(e => logger.error(e));
                                });
                        }).catch(e => logger.error(e));
                    }
                    else {
                        logger.silly(`Ticket ${lobbyState.inhouseState.leagueid} not found on bot.`);
                        lobbyState.state = CONSTANTS.STATE_WAITING_FOR_BOT;
                        lobbyState = await Lobby.unassignBotFromLobby(lobbyState);
                        this[CONSTANTS.MSG_BOT_UNASSIGNED](lobbyState, `Missing league ticket ${lobbyState.inhouseState.leagueid}.`);
                    }
                }
                else {
                    logger.silly(`dotaLobbyId mismatch. lobbyState.dotaLobbyId ${lobbyState.dotaLobbyId} dotaBot.dotaLobbyId ${dotaBot.dotaLobbyId}`);
                    lobbyState.state = CONSTANTS.STATE_WAITING_FOR_BOT;
                    lobbyState = await Lobby.unassignBotFromLobby(lobbyState);
                    await Db.updateBotStatusBySteamId(CONSTANTS.BOT_IN_LOBBY)(dotaBot.steamId64);
                    this[CONSTANTS.MSG_BOT_UNASSIGNED](lobbyState, 'In another lobby.');
                }
            }
            await Db.updateLobby(lobbyState);
            return lobbyState;
        }
        lobbyState.state = CONSTANTS.STATE_WAITING_FOR_BOT;
        await Db.updateLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_BOT_STARTED](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        const dotaBot = this.getBot(lobbyState.botId);
        const players = await Lobby.getPlayers()(lobbyState);
        dotaBot.teamCache = players.reduce(Lobby.reducePlayerToTeamCache(lobbyState.radiantFaction), {});
        await Lobby.mapPlayers(DotaBot.invitePlayer(dotaBot))(lobbyState);
        lobbyState.state = CONSTANTS.STATE_WAITING_FOR_PLAYERS;
        await Db.updateLobby(lobbyState);
        this[CONSTANTS.MSG_LOBBY_INVITES_SENT](lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_BOT_FAILED](_lobbyState) {
        this[CONSTANTS.MSG_BOT_FAILED](_lobbyState);
        return { ..._lobbyState };
    },
    async [CONSTANTS.STATE_WAITING_FOR_PLAYERS](_lobbyState) {
        const lobbyState = { ..._lobbyState };
        const dotaBot = this.getBot(lobbyState.botId);
        if (dotaBot) {
            if (DotaBot.isDotaLobbyReady(dotaBot.teamCache, dotaBot.playerState)) {
                logger.silly('lobby run isDotaLobbyReady true');
                return this.onStartDotaLobby(lobbyState, dotaBot);
            }
        }
        else {
            lobbyState.state = CONSTANTS.STATE_BOT_ASSIGNED;
            await Db.updateLobby(lobbyState);
        }
        return lobbyState;
    },
    [CONSTANTS.STATE_MATCH_IN_PROGRESS]: lobbyStateNoOp,
    [CONSTANTS.STATE_MATCH_ENDED]: lobbyStateNoOp,
    async [CONSTANTS.STATE_MATCH_STATS](_lobbyState) {
        let lobbyState = { ..._lobbyState };
        await MatchTracker.updatePlayerRatings(lobbyState);
        lobbyState.state = CONSTANTS.STATE_COMPLETED;
        await Db.updateLobby(lobbyState);
        await this.botLeaveLobby(lobbyState);
        lobbyState = await Lobby.unassignBotFromLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_MATCH_NO_STATS](_lobbyState) {
        let lobbyState = { ..._lobbyState };
        lobbyState.state = CONSTANTS.STATE_COMPLETED_NO_STATS;
        await Db.updateLobby(lobbyState);
        await this.botLeaveLobby(lobbyState);
        lobbyState = await Lobby.unassignBotFromLobby(lobbyState);
        return lobbyState;
    },
    async [CONSTANTS.STATE_PENDING_KILL](_lobbyState) {
        let lobbyState = { ..._lobbyState };
        if (lobbyState.botId != null) {
            await this.botLeaveLobby(lobbyState);
            lobbyState = await Lobby.unassignBotFromLobby(lobbyState);
        }
        if (lobbyState.channel) {
            await lobbyState.channel.delete();
            lobbyState.channel = null;
        }
        if (lobbyState.role) {
            await lobbyState.role.delete();
            lobbyState.role = null;
        }
        await Lobby.removeQueuers(lobbyState);
        await Lobby.removePlayers(lobbyState);
        lobbyState.channelId = null;
        lobbyState.roleId = null;
        let createLobby = false;
        if ([CONSTANTS.STATE_NEW, CONSTANTS.STATE_WAITING_FOR_QUEUE, CONSTANTS.STATE_BEGIN_READY].indexOf(lobbyState.state !== -1)) {
            lobbyState.lobbyName = SnowflakeUtil.generate().toString();
            createLobby = true;
        }
        lobbyState.state = CONSTANTS.STATE_KILLED;
        await Db.updateLobby(lobbyState);
        if (createLobby) {
            this.onCreateLobbyQueue(lobbyState);
        }
        return lobbyState;
    },
    [CONSTANTS.STATE_KILLED]: lobbyStateNoOp,
    [CONSTANTS.STATE_COMPLETED]: lobbyStateNoOp,
    [CONSTANTS.STATE_COMPLETED_NO_STATS]: lobbyStateNoOp,
    [CONSTANTS.STATE_FAILED]: lobbyStateNoOp,
});

module.exports = {
    LobbyStateTransitions,
    LobbyStateHandlers,
};