penguin-band/index.js
2024-01-30 23:07:02 -06:00

1164 lines
35 KiB
JavaScript

import fs from "fs";
import path, { parse } from "path";
import { Client, GatewayIntentBits, Partials } from "discord.js";
const {
TOKEN,
GUILD_ID,
COMMAND_CHANNEL_ID,
ADDUP_ID,
PICKING_ID,
BLU_ID,
RED_ID,
FK_ID,
CAPTAIN_ID,
} = require("./config.json");
let whitelistStr = require("./config.json").RANKING_WHITELIST;
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Channel, Partials.Message],
});
const backticks = "```";
const rankingsPath = path.join(__dirname, "rankings.json");
const matchString = (str, search) => {
if (!str || !search) return false;
return str.toLowerCase().includes(search.toLowerCase());
};
const findPlayer = (guild, searchName) => {
// search display name
let player = guild.members.cache.find((member) =>
matchString(member.displayName, searchName)
);
if (!player) {
// search global name
player = guild.members.cache.find((member) =>
matchString(member.user.globalName, searchName)
);
}
if (!player) {
// search username
player = guild.members.cache.find((member) =>
matchString(member.user.username, searchName)
);
}
if (!player) {
// match id
player = guild.members.cache.find((member) => member.id === searchName);
}
return player;
};
const getApplicableName = (player) => {
return (
player.displayName ||
player.user.globalName ||
player.user.username ||
player.id
);
};
const avgDiff = (arr) => {
// average absolute difference between all pairs of rank properties in a given array
let sum = 0;
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
if (isNaN(arr[i].rank) || isNaN(arr[j].rank)) {
console.error(`Invalid array passed to avgDiff ${arr}`);
return;
}
sum += Math.abs(arr[i].rank - arr[j].rank);
}
}
return sum / (arr.length * (arr.length - 1)) / 2;
};
// values used for balancing, modifiable
// FUN value: Fluctuating Unfairness Normalization value
// PASSION: Player Ability/Skill Separation Index Offset Number
let funInput = 4; // 1-10
let passionInput = 2; // 1-5
// balance attempt counter, not modifiable
let attempts = 0;
const balanceArrays = (a1, a2) => {
// check to make sure arrays are not empty and that rank properties exist
if (a1.length === 0 || a2.length === 0) {
console.error(`Empty array(s) passed to balanceArrays ${a1} ${a2}`);
return [a1, a2];
}
if (!a1[0].rank || !a2[0].rank) {
console.error(`Rank property missing ${a1} ${a2}`);
return [a1, a2];
}
let modArr1 = [...a1];
let modArr2 = [...a2];
let sum1 = modArr1.reduce((acc, player) => acc + player.rank, 0);
let sum2 = modArr2.reduce((acc, player) => acc + player.rank, 0);
let funValue = funInput * 0.01 + 0.025;
let passion = passionInput * 0.1;
let unequalAllowance = Math.floor((sum1 + sum2) * funValue);
// rebalance until teams fall within constraints
attempts = 0;
while (
Math.abs(avgDiff(modArr1) - avgDiff(modArr2)) > passion ||
(Math.abs(sum1 - sum2) > unequalAllowance && attempts < 1000)
) {
attempts++;
if (attempts > 1000) {
console.log(
`Too many attempts to balance teams
${attempts} ${unequalAllowance} ${passion}
${avgDiff(modArr1)} ${avgDiff(modArr2)}`
);
return [modArr1, modArr2];
} else if (attempts > 600) {
unequalAllowance = (sum1 + sum2) * funValue + 2;
} else if (attempts > 300) {
unequalAllowance = (sum1 + sum2) * funValue + 1;
}
let rerolls = 0;
let ran1 = Math.floor(Math.random() * modArr1.length);
let ran2 = Math.floor(Math.random() * modArr2.length);
// swap randomly if teams seem even
if (Math.abs(sum1 - sum2) < unequalAllowance) {
while (modArr1[ran1].rank === modArr2[ran2].rank && rerolls < 100) {
rerolls++;
ran1 = Math.floor(Math.random() * modArr1.length);
ran2 = Math.floor(Math.random() * modArr2.length);
}
}
// else swap players to minimize difference
else if (sum1 > sum2) {
while (modArr1[ran1].rank <= modArr2[ran2].rank && rerolls < 100) {
rerolls++;
ran1 = Math.floor(Math.random() * modArr1.length);
ran2 = Math.floor(Math.random() * modArr2.length);
}
} else if (sum1 < sum2) {
while (modArr1[ran1].rank >= modArr2[ran2].rank && rerolls < 100) {
rerolls++;
ran1 = Math.floor(Math.random() * modArr1.length);
ran2 = Math.floor(Math.random() * modArr2.length);
}
}
// make the swap
let temp = modArr1[ran1];
modArr1[ran1] = modArr2[ran2];
modArr2[ran2] = temp;
sum1 = modArr1.reduce((acc, player) => acc + player.rank, 0);
sum2 = modArr2.reduce((acc, player) => acc + player.rank, 0);
// if array lengths are uneven, act as if the shorter array is padded with the average of all values
// a length difference of more than 1 between the two arrays should be disallowed elsewhere
if (modArr1.length > modArr2.length) {
let avg = sum2 / modArr2.length;
sum2 += avg;
} else if (modArr1.length < modArr2.length) {
let avg = sum1 / modArr1.length;
sum1 += avg;
}
}
modArr1.sort((a, b) => b.rank - a.rank);
modArr2.sort((a, b) => b.rank - a.rank);
console.log(
`${
attempts > 999 ? "forced" : "successful"
} sort after ${attempts} attempts:
${modArr1.map((i) => i.rank)}
${modArr2.map((i) => i.rank)}
${sum1} ${sum2} (${unequalAllowance})
${avgDiff(modArr1)} ${avgDiff(modArr2)} (${passion})`
);
return [modArr1, modArr2];
};
client.on("ready", () => {
console.log(`Logged in as ${client.user.tag}!`);
console.log(
`Deleting old messages from this bot in command channel: ${COMMAND_CHANNEL_ID}...`
);
let channel = client.channels.cache.get(COMMAND_CHANNEL_ID);
if (!channel) return console.error("(ready) Can't find command channel");
channel.messages
.fetch({ limit: 100 })
.then((messages) => {
messages.forEach((message) => {
if (message.author.id === client.user.id) {
message.delete();
}
});
})
.catch(console.error);
});
// send message on error
client.on("error", (error) => {
console.error(error);
let channel = client.channels.cache.get(COMMAND_CHANNEL_ID);
if (!channel) return console.error("Can't find command channel");
client.channels.cache
.get(COMMAND_CHANNEL_ID)
.send("Internal error")
.catch(console.error);
});
client.on("interactionCreate", async (interaction) => {
const command = interaction.commandName;
if (!interaction.isChatInputCommand()) return;
if (
command === "scout" ||
command === "soldier" ||
command === "pyro" ||
command === "demoman" ||
command === "demo" ||
command === "heavy" ||
command === "engineer" ||
command === "engi" ||
command === "medic" ||
command === "sniper" ||
command === "spy"
) {
// get voice channels
const picking = interaction.guild.channels.cache.find(
(channel) => channel.name === "picking" || channel.id === PICKING_ID
);
if (!picking) return console.error("Can't find channel 'picking'!");
const captain = interaction.guild.channels.cache.find(
(channel) => channel.name === "captains" || channel.id === CAPTAIN_ID
);
if (!captain) return console.error("Can't find channel 'captains'!");
// make sure user is in picking or captain channel
if (
!picking.members.has(interaction.user.id) &&
!captain.members.has(interaction.user.id)
) {
await interaction.reply({
content: "Must be in picking or captains channel to use this command",
ephemeral: true,
});
return;
} else {
await interaction.reply({
content: "Checking picking channel...",
ephemeral: true,
});
// set role name
let roleName = command;
if (command === "demoman") roleName = "demo";
if (command === "engi") roleName = "engineer";
// check each member in picking channel for role
let str = `In picking (${roleName}):`;
for (const member of picking.members.values()) {
if (member.roles.cache.find((role) => role.name === roleName)) {
if (str !== `In picking (${roleName}):`) str += ",";
str += " " + getApplicableName(member);
}
}
if (str === `In picking (${roleName}):`)
str = `None found ¯\\_(ツ)_/¯ (${roleName})`;
// respond
return await interaction.followUp(str);
}
}
if (interaction.channelId !== COMMAND_CHANNEL_ID) {
await interaction.reply({
content: "Wrong channel, or you lack the required permissions",
ephemeral: true,
});
return;
}
if (command === "hello") {
await interaction.reply("world");
}
if (
command === "topicking" ||
command === "end" ||
command === "resetteams"
) {
await interaction.reply("Moving members...");
// get voice channels
const blu = interaction.guild.channels.cache.find(
(channel) => channel.name === "blu" || channel.id === BLU_ID
);
if (!blu) return console.error("Can't find channel 'blu'!");
const red = interaction.guild.channels.cache.find(
(channel) => channel.name === "red" || channel.id === RED_ID
);
if (!red) return console.error("Can't find channel 'red'!");
const addup = interaction.guild.channels.cache.find(
(channel) => channel.name === "add-up" || channel.id === ADDUP_ID
);
if (!addup) return console.error("Can't find channel 'add-up'!");
const picking = interaction.guild.channels.cache.find(
(channel) => channel.name === "picking" || channel.id === PICKING_ID
);
if (!picking) return console.error("Can't find channel 'picking'!");
// get members in voice channel
let members = blu.members.concat(red.members);
if (command !== "resetteams") members = members.concat(addup.members);
if (members.size === 0) {
return await interaction.followUp(
`Found no members in ${
command === "resetteams" ? "blu" : "addup, blu,"
} or red`
);
}
// move members to addup
let idx = 0,
eCount = 0;
const moveToAddup = async (member) => {
try {
await member.voice.setChannel(picking);
} catch (error) {
console.error(error);
eCount++;
}
};
const moveAllToAddup = async () => {
return Promise.all(
Array.from(members, async ([memberId, member]) => {
await moveToAddup(member);
})
);
};
moveAllToAddup().then(() =>
interaction.followUp(
`Moved members in ${
command === "resetteams" ? "blu" : "addup, blu,"
} and red${eCount > 0 ? ` (error moving ${eCount} members)` : ""}`
)
);
}
if (command === "fk" || command === "fatkid") {
// pull users in addup into picking, then randomly select other users in picking to be sat out until there are 18 in picking
await interaction.reply("Randomly choosing fatkids from picking...");
// get voice channels
const addup = interaction.guild.channels.cache.find(
(channel) => channel.name === "add-up" || channel.id === ADDUP_ID
);
const picking = interaction.guild.channels.cache.find(
(channel) => channel.name === "picking" || channel.id === PICKING_ID
);
// get members in voice channel
const addupPlayers = Array.from(addup.members.values());
if (addupPlayers.length === 0 && pickingPlayers.length <= 18) {
return await interaction.followUp("Found no players in addup and picking has 18 or fewer players");
}
const pickingPlayers = Array.from(picking.members.values());
if (pickingPlayers.length === 0) {
return await interaction.followUp("Found no players in picking");
}
let fatkids = [];
// select excess players to be sat out
while (pickingPlayers.length + addupPlayers.length > 18) {
const idx = Math.floor(Math.random() * pickingPlayers.length);
fatkids.push(pickingPlayers.splice(idx, 1)[0]);
}
let errCount = 0;
// move players from addup to picking
for (const newPlayer of addupPlayers) {
try {
await newPlayer.voice.setChannel(picking);
} catch (error) {
console.error(error);
errCount++;
}
}
// move players from picking to fatkid
for (const fk of fatkids) {
try {
if (picking.members.size <= 18) break;
await fk.voice.setChannel(addup);
} catch (error) {
console.error(error);
errCount++;
}
}
interaction.followUp(
`Sat out ${fatkids.length} players${
errCount > 0 ? ` (error moving ${errCount} members)` : ""
}`
);
}
/*if (command === "testfk") {
// debug fk
// pull users in addup into picking, then randomly select other users in picking to be sat out until there are 18 in picking
await interaction.reply("Randomly choosing fatkids from picking...");
// get voice channels
const addup = interaction.guild.channels.cache.find(
(channel) => channel.name === "add-up" || channel.id === ADDUP_ID
);
const picking = interaction.guild.channels.cache.find(
(channel) => channel.name === "picking" || channel.id === PICKING_ID
);
// get members in voice channel
const addupPlayers = Array.from(addup.members.values());
if (addupPlayers.length === 0) {
return await interaction.followUp("Found no players in addup");
}
const pickingPlayers = Array.from(picking.members.values());
if (pickingPlayers.length === 0) {
return await interaction.followUp("Found no players in picking");
}
let fatkids = [];
// select excess players to be sat out
while (pickingPlayers.length + addupPlayers.length > 2) {
const idx = Math.floor(Math.random() * pickingPlayers.length);
fatkids.push(pickingPlayers.splice(idx, 1)[0]);
}
let errCount = 0;
// move players from addup to picking
for (const newPlayer of addupPlayers) {
try {
await newPlayer.voice.setChannel(picking);
} catch (error) {
console.error(error);
errCount++;
}
}
// move players from picking to fatkid
for (const fk of fatkids) {
try {
console.log(addup.members.size, picking.members.size)
if (picking.members.size <= 2) break;
await fk.voice.setChannel(addup);
} catch (error) {
console.error(error);
errCount++;
}
}
interaction.followUp(
`Sat out ${fatkids.length} players${
errCount > 0 ? ` (error moving ${errCount} members)` : ""
}`
);
}*/
if (command === "fklist" || command === "listfk") {
// moves players in picking to fatkid channel, for use in captain pugs
await interaction.reply("Moving fatkids...");
// get voice channels
const picking = interaction.guild.channels.cache.find(
(channel) => channel.name === "picking" || channel.id === PICKING_ID
);
if (!picking) return console.error("Can't find channel 'add-up'!");
const fk = interaction.guild.channels.cache.find(
(channel) => channel.name === "fatkid" || channel.id === FK_ID
);
if (!fk) return console.error("Can't find channel 'fatkid'!");
// get members in voice channel
const members = picking.members;
if (members.size === 0) {
return await interaction.followUp("Found no members in picking");
}
let str = "",
eCount = 0;
const logFk = async (member) => {
try {
await member.voice.setChannel(fk);
if (str.length > 0) str += ", ";
str += getApplicableName(member);
} catch (error) {
console.error(error);
eCount++;
}
};
const logAllFks = async () => {
return Promise.all(
Array.from(members, async ([memberId, member]) => {
await logFk(member);
})
);
};
logAllFks().then(() =>
interaction.followUp(
`Fatkids: ${str}${
eCount > 0 ? ` (error moving ${eCount} members)` : ""
}`
)
);
}
if (command === "pick" || command === "autocaptain") {
await interaction.reply("Picking teams...");
// get voice channels
const picking = interaction.guild.channels.cache.find(
(channel) => channel.name === "picking" || channel.id === PICKING_ID
);
if (!picking) return console.error("Can't find channel 'picking'!");
const blu = interaction.guild.channels.cache.find(
(channel) => channel.name === "blu" || channel.id === BLU_ID
);
if (!blu) return console.error("Can't find channel 'blu'!");
const red = interaction.guild.channels.cache.find(
(channel) => channel.name === "red" || channel.id === RED_ID
);
if (!red) return console.error("Can't find channel 'red'!");
// get players in voice channel
const players = picking.members;
if (players.size !== 18) {
return await interaction.followUp(
`Found ${players.size} players in picking, expected 18`
);
}
// get rankings
let rankings = {};
try {
console.log(`Getting rankings at ${rankingsPath}...`);
if (!fs.existsSync(rankingsPath)) {
fs.writeFileSync(rankingsPath, "{}");
}
rankings = JSON.parse(fs.readFileSync(rankingsPath));
} catch (error) {
console.error(error);
return await message.reply("Error sorting teams");
}
// distribute unranked players evenly, then randomly assign ranked players
let rankedPlayers = [],
bluPlayers = [],
redPlayers = [],
rng = 0;
for (const [playerId, player] of players) {
if (!rankings[playerId]) {
rng = Math.random();
// if teams are even, randomly assign
if (bluPlayers.length === redPlayers.length) {
if (rng < 0.5) {
bluPlayers.push({ player, rank: 0 });
} else {
redPlayers.push({ player, rank: 0 });
}
}
// otherwise, equalize teams
else if (bluPlayers.length < redPlayers.length) {
bluPlayers.push({ player, rank: 0 });
} else {
redPlayers.push({ player, rank: 0 });
}
}
// prepare ranked players for random assignment
else {
rankedPlayers.push(player);
}
}
// slots for ranked players are limited by unranked players, who are sorted differently
let bluRankedPlayers = [],
redRankedPlayers = [],
bluRankSlots = 9 - bluPlayers.length,
redRankSlots = 9 - redPlayers.length;
// create array with players and their ranks
for (const player of rankedPlayers) {
rng = Math.random();
if (rng < 0.5) {
if (bluRankedPlayers.length < bluRankSlots) {
bluRankedPlayers.push({ player, rank: rankings[player.id] });
} else {
redRankedPlayers.push({ player, rank: rankings[player.id] });
}
} else {
if (redRankedPlayers.length < redRankSlots) {
redRankedPlayers.push({ player, rank: rankings[player.id] });
} else {
bluRankedPlayers.push({ player, rank: rankings[player.id] });
}
}
}
if (
bluPlayers.length + bluRankedPlayers.length !== 9 ||
redPlayers.length + redRankedPlayers.length !== 9
) {
console.error(
`Invalid number of players: ${bluPlayers.length} ${bluRankedPlayers.length} ${redPlayers.length} ${redRankedPlayers.length}`
);
return await message.reply("Error sorting teams");
}
// bring teams to reasonable balance
let [bluBalanced, redBalanced] = balanceArrays(
bluRankedPlayers,
redRankedPlayers
);
bluPlayers = bluPlayers.concat(bluBalanced);
redPlayers = redPlayers.concat(redBalanced);
let moveErr = 0;
// move to team voice channels
while (bluPlayers.length > 0) {
const idx = Math.floor(Math.random() * bluPlayers.length);
const player = bluPlayers.splice(idx, 1)[0];
try {
await player.player.voice.setChannel(blu);
} catch (error) {
console.error(error);
moveErr++;
}
}
while (redPlayers.length > 0) {
const idx = Math.floor(Math.random() * redPlayers.length);
const player = redPlayers.splice(idx, 1)[0];
try {
await player.player.voice.setChannel(red);
} catch (error) {
console.error(error);
moveErr++;
}
}
interaction.followUp(
`Players moved into teams${
moveErr > 0 ? ` (error moving ${moveErr} members)` : ""
}`
);
}
if (command === "clear" || command === "bclear") {
await interaction.reply("Clearing messages...");
let channel = client.channels.cache.get(COMMAND_CHANNEL_ID);
if (!channel) return console.error("Can't find command channel");
channel.messages
.fetch({ limit: 100 })
.then((messages) => {
messages.forEach((message) => {
if (message.author.id === client.user.id) {
message.delete();
}
});
})
.catch(console.error);
}
});
/*
* DM commands
* - setrank: saves a player's rank
* - getrank: prints a player's rank
* - rankings: prints all players' ranks
* - fun: prints or sets the FUN value
* - passion: prints or sets the PASSION value
* - simulateteams: simulates autocaptain results
* - whitelist: adds an admin to the whitelist to use DM commands
* - getwhitelist: prints the whitelist
* - clearwhitelist: clears the whitelist completely, security measure
*/
client.on("messageCreate", async (message) => {
if (message.author.bot || message.guild) return;
// check if user is whitelisted
if (!whitelistStr.includes(message.author.id)) {
return;
}
if (message.content.toLowerCase().includes("hello penguin")) {
await message.reply(`Hello ${message.author.username}`);
return;
}
const pickupGuild = client.guilds.cache.get(GUILD_ID);
if (!pickupGuild) {
await message.reply("Could not find guild");
return;
}
const args = message.content.toLowerCase().split(" ");
if (args[0] === "setrank") {
if (args.length < 3) {
await message.reply(
"Invalid number of arguments. usage: `setrank <player name or id> <rank (0-5)>`"
);
return;
}
const player = findPlayer(pickupGuild, args[1]);
if (!player) {
await message.reply(
`Could not find player ${args[1]}. If this issue persists, try copy/pasting the player's Discord handle or user ID`
);
return;
}
const playerId = player.id;
const applicableName = getApplicableName(player);
const rank = parseInt(args[2]);
if (isNaN(rank) || rank < 0 || rank > 5) {
await message.reply(
`Invalid rank ${args[2]}. Supply an integer between 0 and 5`
);
return;
}
// create rankings.json if it doesn't exist
const rankingsPath = path.join(__dirname, "rankings.json");
if (!fs.existsSync(rankingsPath)) {
fs.writeFileSync(rankingsPath, "{}");
}
// rankings.json is a map of player's id to rank
const rankings = JSON.parse(fs.readFileSync(rankingsPath));
rankings[playerId] = rank;
console.log(
`Setting rank of ${applicableName}, ${playerId} to ${rank} at ${rankingsPath}...`
);
new Promise((resolve, reject) => {
fs.writeFile(rankingsPath, JSON.stringify(rankings), (err) => {
if (err) reject(err);
else resolve();
});
}).then((res, err) => {
if (err) {
console.error(err);
message.reply(`Error setting rank: ${err.message}`);
} else {
message.reply(`Set rank of ${applicableName} to ${rank}`);
}
});
}
if (args[0] === "getrank") {
if (args.length < 2) {
await message.reply(
"Invalid number of arguments. Usage: `getrank <player name or id>`"
);
return;
}
const player = findPlayer(pickupGuild, args[1]);
if (!player) {
await message.reply(
`Could not find player ${args[1]}. If this issue persists, try copy/pasting the player's Discord handle or user ID`
);
return;
}
const playerId = player.id;
const applicableName = getApplicableName(player);
try {
console.log(
`Getting rank of ${applicableName}, ${playerId} at ${rankingsPath}...`
);
if (!fs.existsSync(rankingsPath)) {
fs.writeFileSync(rankingsPath, "{}");
}
const rankings = JSON.parse(fs.readFileSync(rankingsPath));
await message.reply(
`${backticks}${applicableName} - ${"*".repeat(
rankings[playerId]
)}${backticks}`
);
} catch (error) {
console.error(error);
await message.reply(`Error getting rank: ${error.message}`);
}
}
if (args[0] === "rankings") {
await message.reply("Getting rankings...");
try {
console.log(`Getting rankings at ${rankingsPath}...`);
if (!fs.existsSync(rankingsPath)) {
fs.writeFileSync(rankingsPath, "{}");
}
const rankings = JSON.parse(fs.readFileSync(rankingsPath));
let players = [];
for (const [playerId, rank] of Object.entries(rankings)) {
if (rank > 0) {
const player = await pickupGuild.members.fetch(playerId);
const applicableName = getApplicableName(player);
players.push({ name: applicableName, rank });
}
}
// sort by rank, then name
players.sort((a, b) => {
if (a.rank === b.rank) {
return a.name.localeCompare(b.name);
}
return b.rank - a.rank;
});
// build string
let str = backticks;
const maxNameLength = Math.max(...players.map((p) => p.name.length));
for (const { name, rank } of players) {
str += `${name.padEnd(maxNameLength, " ")} - ${"*".repeat(rank)}\n`;
}
str += backticks;
if (str === backticks + backticks) str = "No rankings found";
await message.reply(str);
} catch (error) {
console.error(error);
await message.reply(`Error getting rankings: ${error.message}`);
}
}
if (args[0] === "fun") {
if (
args.length < 2 ||
isNaN(args[1]) ||
parseInt(args[1]) < 1 ||
parseInt(args[1]) > 10
)
return await message.reply(
`Current FUN value: ${funInput}\nUsage: \`fun <1-10>\``
);
funInput = parseInt(args[1]);
return await message.reply(`FUN value set to ${funInput}`);
}
if (args[0] === "passion") {
if (
args.length < 2 ||
isNaN(args[1]) ||
parseInt(args[1]) < 1 ||
parseInt(args[1]) > 5
)
return await message.reply(
`Current PASSION: ${passionInput}\nUsage: \`passion <1-5>\``
);
passionInput = parseInt(args[1]);
return await message.reply(`PASSION set to ${passionInput}`);
}
if (args[0] === "simulateteams") {
await message.reply("Simulating teams...");
// get players in voice channel
// const players = picking.members;
// if (players.size !== 18) {
// return await message.reply(
// `Found ${players.size} players in picking, expected 18`
// );
// }
// simulate input is 18 player display names separated by commas
const playerInputs = args[1].split(",");
if (playerInputs.length !== 18) {
return await message.reply(
`Found ${playerInputs.length} players in input, expected 18
\nUsage: \`simulateteams <player1>,<player2>,...,<player18>\``
);
}
// get players from input
let players = new Map();
for (const input of playerInputs) {
const player = findPlayer(pickupGuild, input);
if (!player) {
await message.reply(`Could not find player ${input}`);
return;
}
players.set(player.id, player);
}
// get rankings
let rankings = {};
try {
console.log(`Getting rankings at ${rankingsPath}...`);
if (!fs.existsSync(rankingsPath)) {
fs.writeFileSync(rankingsPath, "{}");
}
rankings = JSON.parse(fs.readFileSync(rankingsPath));
} catch (error) {
console.error(error);
return await message.reply("Error sorting teams");
}
// distribute unranked players evenly, then randomly assign ranked players
let rankedPlayers = [],
bluPlayers = [],
redPlayers = [],
rng = 0;
for (const [playerId, player] of players) {
if (!rankings[playerId]) {
rng = Math.random();
// if teams are even, randomly assign
if (bluPlayers.length === redPlayers.length) {
if (rng < 0.5) {
bluPlayers.push({ player, rank: 0 });
} else {
redPlayers.push({ player, rank: 0 });
}
}
// otherwise, equalize teams
else if (bluPlayers.length < redPlayers.length) {
bluPlayers.push({ player, rank: 0 });
} else {
redPlayers.push({ player, rank: 0 });
}
}
// prepare ranked players for random assignment
else {
rankedPlayers.push(player);
}
}
console.log(
`bluUnranked: ${bluPlayers.length} redUnranked: ${redPlayers.length}`
);
// slots for ranked players are limited by unranked players, who are sorted differently
let bluRankedPlayers = [],
redRankedPlayers = [],
bluRankSlots = 9 - bluPlayers.length,
redRankSlots = 9 - redPlayers.length;
console.log(`bluRankSlots: ${bluRankSlots} redRankSlots: ${redRankSlots}`);
// create array with players and their ranks
for (const player of rankedPlayers) {
rng = Math.random();
console.log(
`rng: ${rng}, bluRankedPlayers: ${bluRankedPlayers.length}, redRankedPlayers: ${redRankedPlayers.length}`
);
if (rng < 0.5) {
if (bluRankedPlayers.length < bluRankSlots) {
bluRankedPlayers.push({ player, rank: rankings[player.id] });
} else {
redRankedPlayers.push({ player, rank: rankings[player.id] });
}
} else {
if (redRankedPlayers.length < redRankSlots) {
redRankedPlayers.push({ player, rank: rankings[player.id] });
} else {
bluRankedPlayers.push({ player, rank: rankings[player.id] });
}
}
}
if (
bluPlayers.length + bluRankedPlayers.length !== 9 ||
redPlayers.length + redRankedPlayers.length !== 9
) {
console.error(
`Invalid number of players: ${bluPlayers.length} ${bluRankedPlayers.length} ${redPlayers.length} ${redRankedPlayers.length}`
);
return await message.reply("Error sorting teams");
}
// bring teams to reasonable balance
let [bluBalanced, redBalanced] = balanceArrays(
bluRankedPlayers,
redRankedPlayers
);
bluPlayers = bluPlayers.concat(bluBalanced);
redPlayers = redPlayers.concat(redBalanced);
let moveErr = 0;
// for sim, print teams after sorting by rank then name
bluPlayers.sort((a, b) => {
if (a.rank === b.rank) {
return a.player.displayName.localeCompare(b.player.displayName);
}
return b.rank - a.rank;
});
redPlayers.sort((a, b) => {
if (a.rank === b.rank) {
return a.player.displayName.localeCompare(b.player.displayName);
}
return b.rank - a.rank;
});
let simString = `${backticks}BLU:`;
// move to team voice channels
while (bluPlayers.length > 0) {
//let idx = Math.floor(Math.random() * bluPlayers.length);
//if (idx === bluPlayers.length) idx--;
let idx = 0;
const player = bluPlayers.splice(idx, 1)[0];
try {
//await player.voice.setChannel(blu);
simString += `\n${getApplicableName(player.player)} - ${"*".repeat(
player.rank
)}`;
} catch (error) {
console.error(error);
moveErr++;
}
}
simString += `\n\nRED:`;
while (redPlayers.length > 0) {
//let idx = Math.floor(Math.random() * redPlayers.length);
//if (idx === redPlayers.length) idx--;
let idx = 0;
const player = redPlayers.splice(idx, 1)[0];
try {
//await player.voice.setChannel(red);
simString += `\n${getApplicableName(player.player)} - ${"*".repeat(
player.rank
)}`;
} catch (error) {
console.error(error);
moveErr++;
}
}
message.reply(
`Simulated teams:\n${simString}${backticks}${
moveErr > 0 ? ` (error moving ${moveErr} members)` : ""
}${attempts > 999 ? "(forced)" : ""}`
);
}
if (args[0] === "whitelist") {
// add user to config.json whitelist
if (args.length < 2) {
await message.reply(
"Invalid number of arguments. Usage: `whitelist <name>`"
);
return;
}
const name = args[1];
const player = findPlayer(pickupGuild, name);
if (!player) {
await message.reply(
`Could not find player ${name}. If this issue persists, try copy/pasting the player's Discord handle or user ID`
);
return;
}
const playerId = player.id;
const applicableName = getApplicableName(player);
if (whitelistStr.includes(playerId)) {
await message.reply(
`User ${applicableName} (${playerId}) is already whitelisted`
);
return;
}
const configPath = path.join(__dirname, "config.json");
const config = JSON.parse(fs.readFileSync(configPath));
whitelistStr += `,${playerId}`;
config.RANKING_WHITELIST = whitelistStr;
new Promise((resolve, reject) => {
fs.writeFile(configPath, JSON.stringify(config), (err) => {
if (err) reject(err);
else resolve();
});
}).then((res, err) => {
if (err) {
console.error(err);
message.reply(`Error whitelisting user: ${err.message}`);
} else {
message.reply(
`Whitelisted user ${applicableName} (${playerId}). If this was done in error, please contact an admin`
);
}
});
}
if (args[0] === "getwhitelist") {
let str = `${backticks}${whitelistStr}${backticks}${backticks}`;
const whitelistIds = whitelistStr.split(",");
for (const id of whitelistIds) {
const player = findPlayer(pickupGuild, id);
if (player) {
str += `\n${getApplicableName(player)}`;
}
}
str += `${backticks}`;
await message.reply(str);
}
if (args[0] === "clearwhitelist") {
if (args[1] !== "confirm") {
await message.reply(
"This command will clear the whitelist and prevent further DM commands. This will require manual intervention to undo. To confirm, use `clearwhitelist confirm`"
);
return;
} else {
const configPath = path.join(__dirname, "config.json");
const config = JSON.parse(fs.readFileSync(configPath));
whitelistStr = "";
config.RANKING_WHITELIST = whitelistStr;
new Promise((resolve, reject) => {
fs.writeFile(configPath, JSON.stringify(config), (err) => {
if (err) reject(err);
else resolve();
});
}).then((res, err) => {
if (err) {
console.error(err);
message.reply(`Error clearing whitelist: ${err.message}`);
} else {
message.reply("Whitelist cleared, please alert an admin");
}
});
}
}
});
client.login(TOKEN);