Source

lib/lobby.js

/**
 * @module lobby
 */

/**
 * @typedef module:lobby.LobbyState
 * @type {Object}
 * @property {module:ihl.InhouseState} inhouseState - The inhouse state the lobby belongs to.
 * @property {external:discordjs.GuildChannel} channel - The discord lobby channel.
 * @property {external:discordjs.Role} role - The discord lobby role.
 * @property {number} readyCheckTime - Start of lobby ready timer.
 * @property {string} state - The lobby state.
 * @property {number} botId - The record id of the bot hosting the lobby.
 * @property {string} queueType - The lobby queue type.
 * @property {string} lobbyName - The lobby name.
 * @property {string} dotaLobbyId - The dota lobby id.
 * @property {string} password - The lobby password.
 * @property {string} captain1UserId - The first captain user record id.
 * @property {string} captain2UserId - The second captain user record id.
 * @property {string} matchId - The match id for the lobby.
 */

const util = require('util');
const Promise = require('bluebird');
const Sequelize = require('sequelize');

const logger = require('./logger');
const shuffle = require('./util/shuffle');
const combinations = require('./util/combinations');
const templateString = require('./util/templateString');
const capitalize = require('./util/capitalize');
const CONSTANTS = require('./constants');
const Guild = require('./guild');
const Db = require('./db');
const { cache } = require('./cache');
const Fp = require('./util/fp');
const AsyncLock = require('async-lock');

const lock = new AsyncLock();

const getLobby = async (lobbyOrState) => {
    if (lobbyOrState instanceof Sequelize.Model) {
        const lobby = await lobbyOrState.reload();
        cache.Lobby.set(lobby.id, lobby);
        return lobby;
    }
    return Db.findLobbyById(lobbyOrState.id);
};

const getPlayers = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getPlayers(options) : []));

const getPlayerByUserId = lobbyOrState => async id => getPlayers({ where: { id } })(lobbyOrState).then(players => (players ? players[0] : null));

const getPlayerBySteamId = lobbyOrState => async steamId64 => getPlayers({ where: { steamId64 } })(lobbyOrState).then(players => (players ? players[0] : null));

const getPlayerByDiscordId = lobbyOrState => async discordId => getPlayers({ where: { discordId } })(lobbyOrState).then(players => (players ? players[0] : null));

const getNoFactionPlayers = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getNoFactionPlayers(options) : []));

const getFaction1Players = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getFaction1Players(options) : []));

const getFaction2Players = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getFaction2Players(options) : []));

const getRadiantPlayers = options => async lobbyOrState => getLobby(lobbyOrState).then((lobby) => {
    if (lobby) {
        if (lobby.radiantFaction === 1) {
            return lobby.getFaction1Players(options);
        }
        return lobby.getFaction2Players(options);
    }
    return [];
});

const getDirePlayers = options => async lobbyOrState => getLobby(lobbyOrState).then((lobby) => {
    if (lobby) {
        if (lobby.radiantFaction === 2) {
            return lobby.getFaction1Players(options);
        }
        return lobby.getFaction2Players(options);
    }
    return [];
});

const getNotReadyPlayers = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getNotReadyPlayers(options) : []));

const getReadyPlayers = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getReadyPlayers(options) : []));

const mapPlayers = fn => async lobbyOrState => Fp.pipeP(getPlayers(), Fp.mapPromise(fn))(lobbyOrState);

const addPlayer = lobbyOrState => async user => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.addPlayer(user) : null));

const removePlayer = lobbyOrState => async user => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.removePlayer(user) : null));

const addPlayers = lobbyOrState => async users => Fp.mapPromise(addPlayer(lobbyOrState))(users);

const addRoleToPlayers = async lobbyState => mapPlayers(Guild.addRoleToUser(lobbyState.inhouseState.guild)(lobbyState.role))(lobbyState);

const updateLobbyPlayer = data => lobbyOrState => async id => getPlayerByUserId(lobbyOrState)(id).then(player => (player ? player.LobbyPlayer.update(data) : null));

const updateLobbyPlayerBySteamId = data => lobbyOrState => async steamId64 => getPlayerBySteamId(lobbyOrState)(steamId64).then(player => (player ? player.LobbyPlayer.update(data) : null));

const setPlayerReady = ready => updateLobbyPlayer({ ready });

const setPlayerFaction = faction => updateLobbyPlayer({ faction });

const sortQueuersAsc = async queuers => Promise.resolve(queuers).then(q => (q ? q.sort((a, b) => a.LobbyQueuer.createdAt - b.LobbyQueuer.createdAt) : []));

const getQueuers = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getQueuers(options) : []));

const getActiveQueuers = options => async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.getActiveQueuers(options) : [])).then(sortQueuersAsc);

const getQueuerByUserId = lobbyOrState => async id => getQueuers({ where: { id } })(lobbyOrState).then(queuers => (queuers ? queuers[0] : null));

const getQueuerBySteamId = lobbyOrState => async steamId64 => getQueuers({ where: { steamId64 } })(lobbyOrState).then(queuers => (queuers ? queuers[0] : null));

const getQueuerByDiscordId = lobbyOrState => async discordId => getQueuers({ where: { discordId } })(lobbyOrState).then(queuers => (queuers ? queuers[0] : null));

const mapQueuers = fn => async lobbyOrState => Fp.pipeP(getQueuers(), Fp.mapPromise(fn))(lobbyOrState);

const mapActiveQueuers = fn => async lobbyOrState => Fp.pipeP(getActiveQueuers(), Fp.mapPromise(fn))(lobbyOrState);

const hasQueuer = lobbyOrState => async user => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.hasQueuer(user) : false));

const addQueuer = lobbyOrState => async user => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.addQueuer(user) : null));

const removeQueuer = lobbyOrState => async user => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.removeQueuer(user) : null));

const addQueuers = lobbyOrState => async users => Fp.mapPromise(addQueuer(lobbyOrState))(users);

const addRoleToQueuers = async lobbyState => mapQueuers(Guild.addRoleToUser(lobbyState.inhouseState.guild)(lobbyState.role))(lobbyState);

const updateLobbyQueuer = data => lobbyOrState => async id => getQueuerByUserId(lobbyOrState)(id).then(queuer => (queuer ? queuer.LobbyQueuer.update(data) : null));

const updateLobbyQueuerBySteamId = data => lobbyOrState => async steamId64 => getQueuerBySteamId(lobbyOrState)(steamId64).then(queuer => (queuer ? queuer.LobbyQueuer.update(data) : null));

const setQueuerActive = active => updateLobbyQueuer({ active });

const removeUserFromQueues = async user => user.setQueues([]);

const removeQueuers = async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.setQueuers([]) : null));

const removePlayers = async lobbyOrState => getLobby(lobbyOrState).then(lobby => (lobby ? lobby.setPlayers([]) : null));

const lobbyQueuerToPlayer = lobbyOrState => async user => Fp.pipeP(Db.updateQueuesForUser(false), addPlayer(lobbyOrState))(user);

const returnPlayerToQueue = lobbyOrState => async user => Fp.pipeP(Db.updateQueuesForUser(true), removePlayer(lobbyOrState))(user);

const returnPlayersToQueue = async lobbyOrState => mapPlayers(returnPlayerToQueue(lobbyOrState))(lobbyOrState);

const isUserInGuild = guild => async (user) => {
    try {
        await Guild.resolveUser(guild)(user);
        return true;
    }
    catch (e) {
        return false;
    }
};

const cleanMissingPlayers = async lobbyState => Fp.pipeP(
    getPlayers(),
    Fp.filterPromise(Fp.negateP(isUserInGuild(lobbyState.inhouseState.guild))),
    Fp.mapPromise(Db.unvouchUser),
    Fp.mapPromise(removePlayer(lobbyState)),
)(lobbyState);

const cleanMissingQueuers = async lobbyState => Fp.pipeP(
    getQueuers(),
    Fp.filterPromise(Fp.negateP(isUserInGuild(lobbyState.inhouseState.guild))),
    Fp.mapPromise(Db.unvouchUser),
    Fp.mapPromise(removeQueuer(lobbyState)),
)(lobbyState);

const checkPlayers = async (lobbyState) => {
    try {
        const players = await getPlayers()(lobbyState);
        if (players.length !== 10) {
            return { ...lobbyState, state: CONSTANTS.STATE_FAILED, failReason: 'checkPlayers: invalid player count {e.toString()}' };
        }
        try {
            await Fp.mapPromise(Guild.resolveUser(lobbyState.inhouseState.guild))(players);
        }
        catch (e) {
            return { ...lobbyState, state: CONSTANTS.STATE_FAILED, failReason: 'checkPlayers: missing player {e.toString()}' };
        }
    }
    catch (e) {
        return { ...lobbyState, state: CONSTANTS.STATE_FAILED, failReason: 'checkPlayers: {e.toString()}' };
    }
    return { ...lobbyState };
};

const validateLobbyPlayers = async (_lobbyState) => {
    let lobbyState;
    switch (_lobbyState.state) {
    case CONSTANTS.STATE_WAITING_FOR_PLAYERS:
        // falls through
    case CONSTANTS.STATE_BOT_STARTED:
        // falls through
    case CONSTANTS.STATE_BOT_FAILED:
        // falls through
    case CONSTANTS.STATE_BOT_ASSIGNED:
        // falls through
    case CONSTANTS.STATE_WAITING_FOR_BOT:
        // falls through
    case CONSTANTS.STATE_TEAMS_SELECTED:
        // falls through
    case CONSTANTS.STATE_AUTOBALANCING:
        // falls through
    case CONSTANTS.STATE_DRAFTING_PLAYERS:
        // falls through
    case CONSTANTS.STATE_SELECTION_PRIORITY:
        // falls through
    case CONSTANTS.STATE_ASSIGNING_CAPTAINS:
        // remove lobby players and add them back as active queuers
        lobbyState = await checkPlayers(_lobbyState);
        if (lobbyState.state === CONSTANTS.STATE_FAILED) {
            logger.silly(`validateLobbyPlayers ${lobbyState.id} failed, returning players to queue`);
            await Fp.pipeP(
                getPlayers(),
                addQueuers(lobbyState),
            )(lobbyState);
            await returnPlayersToQueue(lobbyState);
            lobbyState.state = CONSTANTS.STATE_WAITING_FOR_QUEUE;
            await Db.updateLobby(lobbyState);
        }
        break;
    case CONSTANTS.STATE_CHECKING_READY:
        // falls through
    case CONSTANTS.STATE_BEGIN_READY:
        // remove lobby players and set queuers active
        lobbyState = await checkPlayers(_lobbyState);
        if (lobbyState.state === CONSTANTS.STATE_FAILED) {
            logger.silly(`validateLobbyPlayers ${lobbyState.id} failed, returning players to queue`);
            await returnPlayersToQueue(lobbyState);
            lobbyState.state = CONSTANTS.STATE_WAITING_FOR_QUEUE;
            await Db.updateLobby(lobbyState);
        }
        break;
    case CONSTANTS.STATE_WAITING_FOR_QUEUE:
        // falls through
    case CONSTANTS.STATE_NEW:
        // falls through
    default:
        logger.silly(`validateLobbyPlayers ${_lobbyState.id} default ${_lobbyState.state}`);
        return { ..._lobbyState };
    }
    logger.silly(`validateLobbyPlayers ${lobbyState.id} end ${_lobbyState.state} to ${lobbyState.state}`);
    return lobbyState;
};

const calcBalanceTeams = getPlayerRating => (players) => {
    logger.silly('calcBalanceTeams');
    const combs = combinations(players, 5);
    let bestWeightDiff = 999999;
    let bestPairs = [];
    combs.forEach((comb) => {
        const team1 = comb;
        const team2 = players.filter(i => comb.indexOf(i) < 0);
        const weight1 = team1.reduce((total, player) => total + getPlayerRating(player), 0);
        const weight2 = team2.reduce((total, player) => total + getPlayerRating(player), 0);
        const weightDiff = Math.abs(weight1 - weight2);
        if (weightDiff < bestWeightDiff) {
            bestPairs = [[team1, team2]];
            bestWeightDiff = weightDiff;
        }
        else if (weightDiff === bestWeightDiff) {
            bestPairs.push([team1, team2]);
        }
    });
    shuffle(bestPairs);
    const bestPair = bestPairs.pop();
    logger.silly(`calcBalanceTeams done. ${bestPair.length}`);
    return bestPair;
};

const setTeams = lobbyOrState => async ([team1, team2]) => {
    const lobby = await getLobby(lobbyOrState);
    team1.forEach((player) => {
        // eslint-disable-next-line no-param-reassign
        player.LobbyPlayer = { faction: 1 };
    });
    team2.forEach((player) => {
        // eslint-disable-next-line no-param-reassign
        player.LobbyPlayer = { faction: 2 };
    });
    return lobby.setPlayers(team1.concat(team2), { through: { faction: 0 } });
};

const getPlayerRatingFunction = (matchmakingSystem) => {
    logger.silly(`getPlayerRatingFunction ${matchmakingSystem}`);
    switch (matchmakingSystem) {
    case 'elo':
        return player => player.rating;
    default:
        return player => player.rankTier;
    }
};

const selectCaptainPairFromTiers = captainRankThreshold => getPlayerRating => (tiers) => {
    const keys = Object.keys(tiers).sort();
    logger.silly(`selectCaptainPairFromTiers captainRankThreshold: ${captainRankThreshold} keys: ${keys}`);
    // loop through tiers. lower tier = higher priority
    for (const key of keys) {
        logger.silly(`selectCaptainPairFromTiers checking tier ${key}`);
        const tier = tiers[key];
        // only look at tiers with at least 2 players in them
        if (tier.length >= 2) {
            logger.silly(`selectCaptainPairFromTiers tier ${key} length >= 2`);
            // get all possible pairs within the tier
            let combs = combinations(tier, 2);

            logger.silly(`selectCaptainPairFromTiers combs ${combs.length}`);
            // filter out pairs that exceed skill difference threshold
            combs = combs.filter(([player1, player2]) => Math.abs(getPlayerRating(player1) - getPlayerRating(player2)) <= captainRankThreshold);
            logger.silly(`selectCaptainPairFromTiers filtered combs ${combs.length}`);

            if (combs.length) {
                // select random pair
                shuffle(combs);
                logger.silly(`selectCaptainPairFromTiers shuffled combs ${combs.length}`);

                return combs.pop();
            }
        }
    }
    return [];
};

const sortPlayersByCaptainPriority = (playersWithCaptainPriority) => {
    const tiers = {};
    for (const [player, captainPriority] of playersWithCaptainPriority) {
        if (captainPriority < Infinity) {
            tiers[captainPriority] = tiers[captainPriority] || [];
            tiers[captainPriority].push(player);
        }
    }
    logger.silly(`sortPlayersByCaptainPriority ${Object.keys(tiers)}`);
    return tiers;
};

const roleToCaptainPriority = regexp => (role) => {
    const match = role.name.match(regexp);
    return match ? parseInt(match[1]) : null;
};

const getCaptainPriorityFromRoles = (captainRoleRegexp, roles) => {
    const regexp = new RegExp(captainRoleRegexp);
    const priorities = roles.map(roleToCaptainPriority(regexp)).filter(Number.isFinite);
    logger.silly(`getCaptainPriorityFromRoles ${captainRoleRegexp} ${roles} ${priorities}`);
    return Math.min.apply(null, priorities);
};

const playerToCaptainPriority = guild => captainRoleRegexp => async player => [player, getCaptainPriorityFromRoles(captainRoleRegexp, await Guild.getUserRoles(guild)(player))];

const getPlayersWithCaptainPriority = guild => captainRoleRegexp => async lobbyOrState => mapPlayers(playerToCaptainPriority(guild)(captainRoleRegexp))(lobbyOrState);

const getActiveQueuersWithCaptainPriority = guild => captainRoleRegexp => async lobbyOrState => mapActiveQueuers(playerToCaptainPriority(guild)(captainRoleRegexp))(lobbyOrState);

const checkQueueForCaptains = async lobbyState => Fp.pipeP(
    getActiveQueuersWithCaptainPriority(lobbyState.inhouseState.guild)(lobbyState.inhouseState.captainRoleRegexp),
    sortPlayersByCaptainPriority,
    selectCaptainPairFromTiers(lobbyState.inhouseState.captainRankThreshold)(getPlayerRatingFunction(lobbyState.inhouseState.matchmakingSystem)),
)(lobbyState);

const assignCaptains = async lobbyState => Fp.pipeP(
    getPlayersWithCaptainPriority(lobbyState.inhouseState.guild)(lobbyState.inhouseState.captainRoleRegexp),
    sortPlayersByCaptainPriority,
    selectCaptainPairFromTiers(lobbyState.inhouseState.captainRankThreshold)(getPlayerRatingFunction(lobbyState.inhouseState.matchmakingSystem)),
)(lobbyState);

const calcDefaultGameMode = defaultGameMode => (gameModePreferences) => {
    const gameModeTotals = gameModePreferences.reduce((tally, gameModePreference) => ({
        ...tally,
        [gameModePreference]: (tally[gameModePreference] || 0) + 1,
    }), {});

    let bestGameMode = defaultGameMode;
    let bestCount = gameModeTotals[defaultGameMode] || 0;
    for (const [gameMode, count] of Object.entries(gameModeTotals)) {
        if (count > bestCount) {
            bestCount = count;
            bestGameMode = gameMode;
        }
    }

    logger.silly(`calcDefaultGameMode ${util.inspect(gameModeTotals)} ${bestCount} ${bestGameMode}`);
    return bestGameMode;
};

const autoBalanceTeams = getPlayerRating => async lobbyOrState => Fp.pipeP(
    getPlayers(),
    calcBalanceTeams(getPlayerRating),
    setTeams(lobbyOrState),
)(lobbyOrState);

const getDefaultGameMode = async lobbyState => Fp.pipeP(
    mapPlayers(player => player.gameModePreference),
    calcDefaultGameMode(lobbyState.inhouseState.defaultGameMode),
)(lobbyState);

const assignGameMode = async lobbyState => ({ ...lobbyState, gameMode: await getDefaultGameMode(lobbyState) });

const getDraftingFaction = draftOrder => async (lobbyOrState) => {
    const playersNoTeam = await getNoFactionPlayers()(lobbyOrState);
    const unassignedCount = playersNoTeam.length;
    if (unassignedCount === 0) {
        return 0;
    }
    logger.silly(`getDraftingFaction ${draftOrder}`);
    return draftOrder[draftOrder.length - unassignedCount] === 'A' ? lobbyOrState.playerFirstPick : 3 - lobbyOrState.playerFirstPick;
};

const getFactionCaptain = lobbyOrState => async faction => (faction > 0 && faction <= 2 ? getPlayerByUserId(lobbyOrState)([lobbyOrState.captain1UserId, lobbyOrState.captain2UserId][faction - 1]) : null);

const isPlayerDraftable = lobbyOrState => async (player) => {
    if (player.id === lobbyOrState.captain1UserId || player.id === lobbyOrState.captain2UserId) {
        return CONSTANTS.INVALID_DRAFT_CAPTAIN;
    }
    if (player.LobbyPlayer.faction !== 0) {
        return CONSTANTS.INVALID_DRAFT_PLAYER;
    }
    return CONSTANTS.PLAYER_DRAFTED;
};

const isCaptain = lobbyState => user => user.id === lobbyState.captain1UserId || user.id === lobbyState.captain2UserId;

const isFactionCaptain = lobbyState => faction => user => user.id === lobbyState[`captain${faction}UserId`];

const formatNameForLobby = input => input.replace(/[^0-9a-z]/gi, '').substring(0, 15);

const getLobbyNameFromCaptains = async (captainName1, captainName2, counter) => {
    const name1 = formatNameForLobby(captainName1);
    const name2 = formatNameForLobby(captainName2);
    const lobbyName = `${name1}-${name2}-${counter}`;
    const lobby = await Db.findLobbyByName(lobbyName);
    if (!lobby) {
        return lobbyName;
    }

    return getLobbyNameFromCaptains(captainName1, captainName2, counter + 1);
};

const resetLobbyState = async (_lobbyState) => {
    const lobbyState = { ..._lobbyState };
    switch (lobbyState.state) {
    case CONSTANTS.STATE_MATCH_IN_PROGRESS:
        if (!lobbyState.leagueid) {
            lobbyState.state = CONSTANTS.STATE_MATCH_NO_STATS;
            logger.silly(`resetLobbyState ${_lobbyState.id} ${_lobbyState.state} to ${lobbyState.state}`);
            break;
        }
        // falls through
    case CONSTANTS.STATE_WAITING_FOR_PLAYERS:
        // falls through
    case CONSTANTS.STATE_BOT_STARTED:
        lobbyState.state = CONSTANTS.STATE_WAITING_FOR_BOT;
        logger.silly(`resetLobbyState ${_lobbyState.id} ${_lobbyState.state} to ${lobbyState.state}`);
        break;
    case CONSTANTS.STATE_CHECKING_READY:
        lobbyState.state = CONSTANTS.STATE_BEGIN_READY;
        logger.silly(`resetLobbyState ${_lobbyState.id} ${_lobbyState.state} to ${lobbyState.state}`);
        break;
    default:
        lobbyState.state = lobbyState.state || CONSTANTS.STATE_NEW;
        break;
    }
    await Db.updateLobby(lobbyState);
    return lobbyState;
};

const isReadyCheckTimedOut = lobbyState => Date.now() - lobbyState.readyCheckTime >= lobbyState.inhouseState.readyCheckTimeout;

const createLobbyState = inhouseState => ({ channel, role }) => lobby => ({
    inhouseState: { ...inhouseState },
    ...(lobby instanceof Sequelize.Model ? lobby.get({ plain: true }) : lobby),
    channel,
    role,
    channelId: channel.id,
    roleId: role.id,
});

const lobbyToLobbyState = inhouseState => async (lobby) => {
    let channel;
    let role;
    try {
        await lock.acquire(`lobby-${lobby.id}`, async () => {
            if (lobby.channelId) {
                channel = Guild.findChannel(inhouseState.guild)(lobby.channelId);
            }
            if (channel) {
                if (channel.name !== lobby.lobbyName) {
                    channel = await Guild.setChannelName(lobby.lobbyName)(channel);
                }
            }
            else {
                channel = await Guild.findOrCreateChannelInCategory(inhouseState.guild, inhouseState.category, lobby.lobbyName);
                await Db.updateLobbyChannel(lobby)(channel);
            }
        });
        await lock.acquire(`lobby-${lobby.id}`, async () => {
            if (lobby.roleId) {
                role = Guild.findRole(inhouseState.guild)(lobby.roleId);
            }
            if (role) {
                if (role.name !== lobby.lobbyName) {
                    role = await Fp.pipeP(
                        Guild.setRoleName(lobby.lobbyName),
                        Guild.setRolePermissions([]),
                        Guild.setRoleMentionable(true),
                    )(role);
                }
            }
            else {
                role = await Guild.makeRole(inhouseState.guild)([])(true)(lobby.lobbyName);
                await Db.updateLobbyRole(lobby)(role);
            }
        });
        const lobbyState = await createLobbyState(inhouseState)({ channel, role })(lobby);
        const badQueuers = await cleanMissingQueuers(lobbyState);
        const queuers = await getQueuers()(lobbyState);
        const badPlayers = await cleanMissingPlayers(lobbyState);
        const players = await getPlayers()(lobbyState);
        logger.silly(`lobbyToLobbyState ${lobby.id}, ${lobbyState.lobbyName}, ${queuers.length}, ${badQueuers.length}, ${players.length}, ${badPlayers.length}, ${lobbyState.captain1UserId}, ${lobbyState.captain2UserId}`);

        if ([CONSTANTS.STATE_NEW, CONSTANTS.STATE_WAITING_FOR_QUEUE].indexOf(lobbyState.state) !== -1) {
            await Guild.setChannelViewable(inhouseState.guild)(true)(channel);
        }
        else {
            await Guild.setChannelViewable(inhouseState.guild)(false)(channel);
            await Guild.setChannelViewableForRole(inhouseState.adminRole)(true)(channel);
            await Guild.setChannelViewableForRole(role)(true)(channel);
        }
        await addRoleToPlayers(lobbyState);
        return lobbyState;
    }
    catch (e) {
        logger.error(e);
        await Db.updateLobbyFailed(lobby)('lobbyToLobbyState: {e.toString()}');
        if (channel) {
            await channel.send('Lobby failed to load.');
            await Guild.setChannelViewable(inhouseState.guild)(false)(channel);
        }
        throw e;
    }
};

const forceLobbyDraft = async (_lobbyState, captain1, captain2) => {
    const lobbyState = { ..._lobbyState };
    const states = [
        CONSTANTS.STATE_ASSIGNING_CAPTAINS,
        CONSTANTS.STATE_SELECTION_PRIORITY,
        CONSTANTS.STATE_DRAFTING_PLAYERS,
        CONSTANTS.STATE_AUTOBALANCING,
        CONSTANTS.STATE_TEAMS_SELECTED,
        CONSTANTS.STATE_WAITING_FOR_BOT,
        CONSTANTS.STATE_BOT_ASSIGNED,
        CONSTANTS.STATE_BOT_STARTED,
        CONSTANTS.STATE_BOT_FAILED,
        CONSTANTS.STATE_WAITING_FOR_PLAYERS,
    ];
    if (states.indexOf(lobbyState.state) !== -1) {
        lobbyState.state = CONSTANTS.STATE_SELECTION_PRIORITY;
        lobbyState.selectionPriority = 0;
        lobbyState.playerFirstPick = 0;
        lobbyState.firstPick = 0;
        lobbyState.radiantFaction = 0;
        lobbyState.captain1UserId = captain1.id;
        lobbyState.captain2UserId = captain2.id;
    }
    return lobbyState;
};

const createChallengeLobby = async ({ inhouseState, captain1, captain2, challenge }) => {
    const captainMember1 = await Guild.resolveUser(inhouseState.guild)(captain1.discordId);
    const captainMember2 = await Guild.resolveUser(inhouseState.guild)(captain2.discordId);
    const lobbyName = await getLobbyNameFromCaptains(captainMember1.displayName, captainMember2.displayName, 1);
    const lobby = await Db.findOrCreateLobbyForGuild(inhouseState.guild.id, CONSTANTS.QUEUE_TYPE_CHALLENGE, lobbyName);
    await Db.setChallengeAccepted(challenge);
    await addQueuers(lobby)([captain1, captain2]);
    const lobbyState = await lobbyToLobbyState(inhouseState)(lobby);
    await Guild.setChannelPosition(1)(lobbyState.channel);
    lobbyState.captain1UserId = captain1.id;
    lobbyState.captain2UserId = captain2.id;
    await Db.updateLobby(lobbyState);
    return lobbyState;
};

const removeLobbyPlayersFromQueues = async lobbyOrState => mapPlayers(removeUserFromQueues)(lobbyOrState);

const assignLobbyName = async (lobbyState) => {
    let lobbyName = templateString(lobbyState.inhouseState.lobbyNameTemplate)({ lobbyId: lobbyState.id });
    lobbyName = lobbyName.toLowerCase().replace(/ /g, '-').replace(/[^0-9a-z-]/gi, '');
    return { ...lobbyState, lobbyName };
};

const reducePlayerToTeamCache = radiantFaction => (_teamCache, player) => {
    const teamCache = { ..._teamCache };
    teamCache[player.steamId64] = player.LobbyPlayer.faction === radiantFaction ? 1 : 2;
    return teamCache;
};

const createLobbyStateMessage = async (lobbyState) => {
    let topic = `Status: ${lobbyState.state.replace('STATE_', '').split('_').map(capitalize).join(' ')}.`;
    const queuers = await getActiveQueuers()(lobbyState);
    if (queuers.length) {
        topic += `\n${queuers.length} in queue: ${queuers.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
    }
    const captains = [];
    if (lobbyState.captain1UserId) {
        captains.push(await Db.findUserById(lobbyState.captain1UserId));
    }
    if (lobbyState.captain2UserId) {
        captains.push(await Db.findUserById(lobbyState.captain2UserId));
    }
    if (captains.length) {
        topic += `\nCaptains: ${captains.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
    }
    if (lobbyState.state === CONSTANTS.STATE_BEGIN_READY || lobbyState.state === CONSTANTS.STATE_CHECKING_READY) {
        const readyPlayers = await getReadyPlayers()(lobbyState);
        if (readyPlayers.length) {
            topic += `\n${readyPlayers.length} ready players: ${readyPlayers.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
        }
        const notReadyPlayers = await getNotReadyPlayers()(lobbyState);
        if (notReadyPlayers.length) {
            topic += `\n${notReadyPlayers.length} players not ready: ${notReadyPlayers.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
        }
    }
    if (lobbyState.state === CONSTANTS.STATE_DRAFTING_PLAYERS) {
        const playersNoTeam = await getNoFactionPlayers()(lobbyState);
        if (playersNoTeam.length) {
            topic += `\n${playersNoTeam.length} unpicked players: ${playersNoTeam.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
        }
    }
    const radiant = await getRadiantPlayers()(lobbyState);
    if (radiant.length) {
        topic += `\nRadiant: ${radiant.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
    }
    const dire = await getDirePlayers()(lobbyState);
    if (dire.length) {
        topic += `\nDire: ${dire.map(user => Guild.userToDisplayName(lobbyState.inhouseState.guild)(user)).join(', ')}.`;
    }
    if ([CONSTANTS.STATE_BOT_ASSIGNED,
        CONSTANTS.STATE_WAITING_FOR_BOT,
        CONSTANTS.STATE_BOT_STARTED,
        CONSTANTS.STATE_WAITING_FOR_PLAYERS].indexOf(lobbyState.state) !== -1) {
        topic += `\nLobby Name: ${lobbyState.lobbyName} Password: ${lobbyState.password}`;
    }
    return topic;
};

const setLobbyTopic = async (lobbyState) => {
    if (lobbyState.state === CONSTANTS.STATE_KILLED) return;
    if (!lobbyState.channel) return;
    const topic = await createLobbyStateMessage(lobbyState);
    await Guild.setChannelTopic(topic)(lobbyState.channel);
};

const assignBotToLobby = lobbyState => async (botId) => {
    await Db.assignBotToLobby(lobbyState)(botId);
    return { ...lobbyState, botId };
};

const unassignBotFromLobby = async (lobbyState) => {
    if (lobbyState.botId) {
        await Db.unassignBotFromLobby(lobbyState)(lobbyState.botId);
    }
    return { ...lobbyState, botId: null, dotaLobbyId: null };
};

module.exports = {
    getLobby,
    getPlayers,
    getPlayerByUserId,
    getPlayerBySteamId,
    getPlayerByDiscordId,
    getNoFactionPlayers,
    getFaction1Players,
    getFaction2Players,
    getRadiantPlayers,
    getDirePlayers,
    getNotReadyPlayers,
    getReadyPlayers,
    mapPlayers,
    addPlayer,
    removePlayer,
    addPlayers,
    addRoleToPlayers,
    updateLobbyPlayer,
    updateLobbyPlayerBySteamId,
    setPlayerReady,
    setPlayerFaction,
    sortQueuersAsc,
    getQueuers,
    getActiveQueuers,
    getQueuerByUserId,
    getQueuerBySteamId,
    getQueuerByDiscordId,
    mapQueuers,
    mapActiveQueuers,
    hasQueuer,
    addQueuer,
    removeQueuer,
    addQueuers,
    addRoleToQueuers,
    updateLobbyQueuer,
    updateLobbyQueuerBySteamId,
    setQueuerActive,
    removeUserFromQueues,
    removeQueuers,
    removePlayers,
    lobbyQueuerToPlayer,
    returnPlayerToQueue,
    returnPlayersToQueue,
    isUserInGuild,
    cleanMissingPlayers,
    cleanMissingQueuers,
    checkPlayers,
    validateLobbyPlayers,
    calcBalanceTeams,
    setTeams,
    getPlayerRatingFunction,
    selectCaptainPairFromTiers,
    sortPlayersByCaptainPriority,
    roleToCaptainPriority,
    getCaptainPriorityFromRoles,
    playerToCaptainPriority,
    getPlayersWithCaptainPriority,
    getActiveQueuersWithCaptainPriority,
    checkQueueForCaptains,
    assignCaptains,
    calcDefaultGameMode,
    autoBalanceTeams,
    getDefaultGameMode,
    assignGameMode,
    getDraftingFaction,
    getFactionCaptain,
    isPlayerDraftable,
    isCaptain,
    isFactionCaptain,
    formatNameForLobby,
    getLobbyNameFromCaptains,
    resetLobbyState,
    isReadyCheckTimedOut,
    createLobbyState,
    lobbyToLobbyState,
    createChallengeLobby,
    forceLobbyDraft,
    removeLobbyPlayersFromQueues,
    assignLobbyName,
    reducePlayerToTeamCache,
    createLobbyStateMessage,
    setLobbyTopic,
    assignBotToLobby,
    unassignBotFromLobby,
};