From 021b9192af23f533d0fa5f3c67d7568c4d593e14 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 2 Jun 2024 23:54:14 -1000 Subject: [PATCH 1/6] Add env variables to begin prep for multi-game support --- .env.example | 3 +++ README.md | 2 +- src/config/env.ts | 2 ++ src/events/ready.ts | 3 +++ src/lib/googleSheet.ts | 1 + src/lib/setup.ts | 4 +++- 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 40d5ba5..bbf2e37 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,6 @@ DISCORD_CLIENT_ID= DISCORD_GUILD_ID= DISCORD_CHANNEL_ID= DISCORD_CATEGORY_ID= + +GAME_NAME= +TEAMS_PER_ALLIANCE= diff --git a/README.md b/README.md index 19e0187..400a3ad 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Discord bot for running simulation robotics tournaments in xRC Sim. Designed t To be used for competitions and tournaments in the Unity-based game [xRC Simulator](http://xrcsimulator.org/). Used in online [SRC events](https://secondrobotics.org). -Currently configured to be used with xRC Sim Charged Up, would need modifications to be used with other games. +Can be configured to be used with multiple games from xRC Simulator. Powered by [Discord.js](https://discord.js.org/) and [Google Sheets API](https://developers.google.com/sheets/api). Based on the [NicholasBottone/xRCSim-Tourney-Runner](https://github.com/NicholasBottone/xRCSim-Tourney-Runner) CLI tool. diff --git a/src/config/env.ts b/src/config/env.ts index ca1562d..396890b 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -12,6 +12,8 @@ const envSchema = z.object({ DISCORD_GUILD_ID: z.string().min(1), DISCORD_CHANNEL_ID: z.string().min(1), DISCORD_CATEGORY_ID: z.string().min(1), + GAME_NAME: z.enum(["CHARGED UP"]), + TEAMS_PER_ALLIANCE: z.number().int(), }); envSchema.parse(process.env); diff --git a/src/events/ready.ts b/src/events/ready.ts index aff5b8c..2ec8b2e 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -7,5 +7,8 @@ export const once = true; export const execute = (client: Client) => { logger.info(`Ready! Logged in as ${client.user?.tag}`); + logger.info( + `Configured for ${process.env.GAME_NAME} (${process.env.TEAMS_PER_ALLIANCE} player alliances)` + ); client.user?.setActivity("xRC Simulator"); }; diff --git a/src/lib/googleSheet.ts b/src/lib/googleSheet.ts index 0ea3896..0f63e35 100644 --- a/src/lib/googleSheet.ts +++ b/src/lib/googleSheet.ts @@ -106,6 +106,7 @@ export async function setupConnection() { alliancesSheet, playoffScheduleSheet, playoffMatchesSheet, + sheetTitle: doc.title, }; } diff --git a/src/lib/setup.ts b/src/lib/setup.ts index 5610d1e..9da369c 100644 --- a/src/lib/setup.ts +++ b/src/lib/setup.ts @@ -12,7 +12,9 @@ export async function setupGoogleSheets() { } const sheetsUrl = `https://docs.google.com/spreadsheets/d/${process.env.GOOGLE_SHEET_DOC_ID}`; - logger.info(`Connected to Google Sheets at ${sheetsUrl}`); + logger.info( + `Connected to Google Sheets at ${sheets.sheetTitle} (${sheetsUrl})` + ); return sheets; } From 99843f1c3e39e66bc477ac4721b0949b067d54b1 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 9 Jun 2024 04:28:38 -0400 Subject: [PATCH 2/6] Add support for expansion to multiple game types --- src/lib/{field.ts => field/chargedUp.ts} | 24 ++------------ src/lib/field/index.ts | 33 +++++++++++++++++++ src/lib/{match.ts => match/chargedUp.ts} | 0 src/lib/match/index.ts | 24 ++++++++++++++ .../chargedUp.ts} | 4 +-- src/lib/resultEmbed/index.ts | 18 ++++++++++ 6 files changed, 80 insertions(+), 23 deletions(-) rename src/lib/{field.ts => field/chargedUp.ts} (90%) create mode 100644 src/lib/field/index.ts rename src/lib/{match.ts => match/chargedUp.ts} (100%) create mode 100644 src/lib/match/index.ts rename src/lib/{resultEmbed.ts => resultEmbed/chargedUp.ts} (96%) create mode 100644 src/lib/resultEmbed/index.ts diff --git a/src/lib/field.ts b/src/lib/field/chargedUp.ts similarity index 90% rename from src/lib/field.ts rename to src/lib/field/chargedUp.ts index 5f727f4..3a0c940 100644 --- a/src/lib/field.ts +++ b/src/lib/field/chargedUp.ts @@ -1,12 +1,10 @@ import fs from "fs/promises"; import fsSync from "fs"; import type { GoogleSpreadsheetRow } from "google-spreadsheet"; -import type { Match } from "./match"; +import type { Match } from "../match/chargedUp"; -export const SUSTAINABILITY_BONUS_RP = 9; -export const ACTIVATION_BONUS_RP = 32; - -export const PLAYOFF_MATCHES_BEFORE_FINALS = 13; +const SUSTAINABILITY_BONUS_RP = 9; +const ACTIVATION_BONUS_RP = 32; export async function getMatchData( scheduledMatch: GoogleSpreadsheetRow, @@ -173,19 +171,3 @@ export async function getMatchData( return match; } - -export async function setMatchNumber(matchType: string, matchNumber: number) { - const type = - matchType === "Qual" - ? "Quals" - : matchNumber > PLAYOFF_MATCHES_BEFORE_FINALS - ? "Finals" - : "Playoff"; - - fsSync.existsSync("TourneyData/") || (await fs.mkdir("TourneyData/")); - await fs.writeFile("TourneyData/MatchNumber.txt", `${type} ${matchNumber}`); - await fs.writeFile( - "TourneyData/PrevMatchNumber.txt", - `${type} ${matchNumber - 1}` - ); -} diff --git a/src/lib/field/index.ts b/src/lib/field/index.ts new file mode 100644 index 0000000..c3b734f --- /dev/null +++ b/src/lib/field/index.ts @@ -0,0 +1,33 @@ +import fs from "fs/promises"; +import fsSync from "fs"; + +import { getMatchData as chargedUpGetMatchData } from "./chargedUp"; + +export const PLAYOFF_MATCHES_BEFORE_FINALS = 13; + +export async function setMatchNumber(matchType: string, matchNumber: number) { + const type = + matchType === "Qual" + ? "Quals" + : matchNumber > PLAYOFF_MATCHES_BEFORE_FINALS + ? "Finals" + : "Playoff"; + + fsSync.existsSync("TourneyData/") || (await fs.mkdir("TourneyData/")); + await fs.writeFile("TourneyData/MatchNumber.txt", `${type} ${matchNumber}`); + await fs.writeFile( + "TourneyData/PrevMatchNumber.txt", + `${type} ${matchNumber - 1}` + ); +} + +let gameGetMatchData; + +switch (process.env.GAME_NAME) { + case "CHARGED UP": + default: + gameGetMatchData = chargedUpGetMatchData; + break; +} + +export const getMatchData = gameGetMatchData; diff --git a/src/lib/match.ts b/src/lib/match/chargedUp.ts similarity index 100% rename from src/lib/match.ts rename to src/lib/match/chargedUp.ts diff --git a/src/lib/match/index.ts b/src/lib/match/index.ts new file mode 100644 index 0000000..f2c77d2 --- /dev/null +++ b/src/lib/match/index.ts @@ -0,0 +1,24 @@ +import { + type Match as chargedUpMatch, + headerValues as chargedUpHeaderValues, + matchToArray as chargedUpMatchToArray, + saveMatchToRow as chargedUpSaveMatchToRow, +} from "./chargedUp"; + +let gameHeaderValues; +let gameMatchToArray; +let gameSaveMatchToRow; + +switch (process.env.GAME_NAME) { + case "CHARGED UP": + default: + gameHeaderValues = chargedUpHeaderValues; + gameMatchToArray = chargedUpMatchToArray; + gameSaveMatchToRow = chargedUpSaveMatchToRow; + break; +} + +export type Match = chargedUpMatch; // | otherMatch; +export const headerValues = gameHeaderValues; +export const matchToArray = gameMatchToArray; +export const saveMatchToRow = gameSaveMatchToRow; diff --git a/src/lib/resultEmbed.ts b/src/lib/resultEmbed/chargedUp.ts similarity index 96% rename from src/lib/resultEmbed.ts rename to src/lib/resultEmbed/chargedUp.ts index c681c05..d0383e1 100644 --- a/src/lib/resultEmbed.ts +++ b/src/lib/resultEmbed/chargedUp.ts @@ -1,6 +1,6 @@ import { EmbedBuilder, type Guild } from "discord.js"; -import type { Match } from "./match"; -import { PLAYOFF_MATCHES_BEFORE_FINALS } from "./field"; +import type { Match } from "../match"; +import { PLAYOFF_MATCHES_BEFORE_FINALS } from "../field"; const codeBlock = (str: string) => `\`\`\`\n${str}\n\`\`\``; diff --git a/src/lib/resultEmbed/index.ts b/src/lib/resultEmbed/index.ts new file mode 100644 index 0000000..7b2c1f0 --- /dev/null +++ b/src/lib/resultEmbed/index.ts @@ -0,0 +1,18 @@ +import { + sendQualMatchEmbed as chargedUpSendQualMatchEmbed, + sendPlayoffMatchEmbed as chargedUpSendPlayoffMatchEmbed, +} from "./chargedUp"; + +let gameSendQualMatchEmbed; +let gameSendPlayoffMatchEmbed; + +switch (process.env.GAME_NAME) { + case "CHARGED UP": + default: + gameSendQualMatchEmbed = chargedUpSendQualMatchEmbed; + gameSendPlayoffMatchEmbed = chargedUpSendPlayoffMatchEmbed; + break; +} + +export const sendQualMatchEmbed = gameSendQualMatchEmbed; +export const sendPlayoffMatchEmbed = gameSendPlayoffMatchEmbed; From 8f93f296838b9cd1f694663396bb440318f62719 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 9 Jun 2024 05:24:34 -0400 Subject: [PATCH 3/6] Add crescendo option --- src/config/env.ts | 2 +- src/lib/field/crescendo.ts | 151 +++++++++++++++++++++++++++++++ src/lib/field/index.ts | 6 +- src/lib/match/crescendo.ts | 120 ++++++++++++++++++++++++ src/lib/match/index.ts | 31 +++++-- src/lib/resultEmbed/chargedUp.ts | 2 +- src/lib/resultEmbed/crescendo.ts | 111 +++++++++++++++++++++++ src/lib/resultEmbed/index.ts | 14 ++- 8 files changed, 423 insertions(+), 14 deletions(-) create mode 100644 src/lib/field/crescendo.ts create mode 100644 src/lib/match/crescendo.ts create mode 100644 src/lib/resultEmbed/crescendo.ts diff --git a/src/config/env.ts b/src/config/env.ts index 396890b..f980377 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -12,7 +12,7 @@ const envSchema = z.object({ DISCORD_GUILD_ID: z.string().min(1), DISCORD_CHANNEL_ID: z.string().min(1), DISCORD_CATEGORY_ID: z.string().min(1), - GAME_NAME: z.enum(["CHARGED UP"]), + GAME_NAME: z.enum(["CHARGED UP", "CRESCENDO"]), TEAMS_PER_ALLIANCE: z.number().int(), }); diff --git a/src/lib/field/crescendo.ts b/src/lib/field/crescendo.ts new file mode 100644 index 0000000..1d698b3 --- /dev/null +++ b/src/lib/field/crescendo.ts @@ -0,0 +1,151 @@ +import fs from "fs/promises"; +import fsSync from "fs"; +import type { GoogleSpreadsheetRow } from "google-spreadsheet"; +import type { Match } from "../match/crescendo"; + +const MELODY_BONUS_RP = 25; +const ENSEMBLE_BONUS_RP = 10; + +export async function getMatchData( + scheduledMatch: GoogleSpreadsheetRow, + dataDirectory: string, + matchNumber: number +) { + if (!fsSync.existsSync(dataDirectory)) { + throw new Error(`Data directory ${dataDirectory} does not exist`); + } + + if (!fsSync.existsSync(`${dataDirectory}/Score_R.txt`)) { + throw new Error( + `Data directory ${dataDirectory} is not populated with data` + ); + } + + const redAlliance = [ + scheduledMatch["Red 1"], + scheduledMatch["Red 2"], + scheduledMatch["Red 3"], + ]; + const blueAlliance = [ + scheduledMatch["Blue 1"], + scheduledMatch["Blue 2"], + scheduledMatch["Blue 3"], + ]; + + // // Sort player contributions (OPR) + // const redAlphabetized = redAlliance.slice().sort(); + // const blueAlphabetized = blueAlliance.slice().sort(); + + // const contribAlphabetized = fs + // .readFileSync(`${dataDirectory}/OPR.txt`, "utf8") + // .split("\n") + // .map((line) => line.split(": ")[1]); + // const unsortedContribRed = contribAlphabetized.slice(0, 3); + // const unsortedContribBlue = contribAlphabetized.slice(3, 6); + // const contribRed = unsortedContribRed.slice(); + // const contribBlue = unsortedContribBlue.slice(); + + // for (let i = 0; i < 3; i++) { + // const redIndex = redAlliance.indexOf(redAlphabetized[i]); + // const blueIndex = blueAlliance.indexOf(blueAlphabetized[i]); + // contribRed[redIndex] = unsortedContribRed[i]; + // contribBlue[blueIndex] = unsortedContribBlue[i]; + // } + + // Count game pieces (notes) + const piecesRed = + parseInt(await fs.readFile(`${dataDirectory}/Aamp_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Aspeaker_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Tamp_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Tspeaker_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Tspeakeramp_R.txt`, "utf8")); + const piecesBlue = + parseInt(await fs.readFile(`${dataDirectory}/Aamp_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Aspeaker_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Tamp_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Tspeaker_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Tspeakeramp_B.txt`, "utf8")); + + // Calculate endgame points + const endRed = parseInt( + await fs.readFile(`${dataDirectory}/End_R.txt`, "utf8") + ); + const endBlue = parseInt( + await fs.readFile(`${dataDirectory}/End_B.txt`, "utf8") + ); + + // Calculate auto points + const autoRed = parseInt( + await fs.readFile(`${dataDirectory}/Auto_R.txt`, "utf8") + ); + const autoBlue = parseInt( + await fs.readFile(`${dataDirectory}/Auto_B.txt`, "utf8") + ); + + // Calculate ranking points + const scoreRed = parseInt( + await fs.readFile(`${dataDirectory}/Score_R.txt`, "utf8") + ); + const scoreBlue = parseInt( + await fs.readFile(`${dataDirectory}/Score_B.txt`, "utf8") + ); + + const rpRedBonus = + (piecesRed >= MELODY_BONUS_RP ? 1 : 0) + + (endRed >= ENSEMBLE_BONUS_RP ? 1 : 0); + const rpRed = + rpRedBonus + (scoreRed > scoreBlue ? 2 : scoreRed === scoreBlue ? 1 : 0); + + const rpBlueBonus = + (piecesBlue >= MELODY_BONUS_RP ? 1 : 0) + + (endBlue >= ENSEMBLE_BONUS_RP ? 1 : 0); + const rpBlue = + rpBlueBonus + (scoreBlue > scoreRed ? 2 : scoreBlue === scoreRed ? 1 : 0); + + // Calculate tiebreakers + const penaltyRed = + (parseInt(await fs.readFile(`${dataDirectory}/Fouls_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Resets_R.txt`, "utf8"))) * + 5; + const penaltyBlue = + (parseInt(await fs.readFile(`${dataDirectory}/Fouls_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Resets_B.txt`, "utf8"))) * + 5; + + const tiebreakerRed = scoreRed - penaltyRed; + const tiebreakerBlue = scoreBlue - penaltyBlue; + + const match: Match = { + matchNumber, + red1: redAlliance[0], + red2: redAlliance[1], + red3: redAlliance[2], + blue1: blueAlliance[0], + blue2: blueAlliance[1], + blue3: blueAlliance[2], + redScore: scoreRed, + blueScore: scoreBlue, + redPenalty: penaltyRed, + bluePenalty: penaltyBlue, + redAuto: autoRed, + blueAuto: autoBlue, + redTeleop: parseInt( + await fs.readFile(`${dataDirectory}/Tele_R.txt`, "utf8") + ), + blueTeleop: parseInt( + await fs.readFile(`${dataDirectory}/Tele_B.txt`, "utf8") + ), + redEnd: endRed, + blueEnd: endBlue, + redGamePieces: piecesRed, + blueGamePieces: piecesBlue, + redRP: rpRed, + blueRP: rpBlue, + redTiebreaker: tiebreakerRed, + blueTiebreaker: tiebreakerBlue, + redBonusRP: rpRedBonus, + blueBonusRP: rpBlueBonus, + }; + + return match; +} diff --git a/src/lib/field/index.ts b/src/lib/field/index.ts index c3b734f..2943f50 100644 --- a/src/lib/field/index.ts +++ b/src/lib/field/index.ts @@ -2,6 +2,7 @@ import fs from "fs/promises"; import fsSync from "fs"; import { getMatchData as chargedUpGetMatchData } from "./chargedUp"; +import { getMatchData as crescendoGetMatchData } from "./crescendo"; export const PLAYOFF_MATCHES_BEFORE_FINALS = 13; @@ -25,9 +26,12 @@ let gameGetMatchData; switch (process.env.GAME_NAME) { case "CHARGED UP": - default: gameGetMatchData = chargedUpGetMatchData; break; + case "CRESCENDO": + default: + gameGetMatchData = crescendoGetMatchData; + break; } export const getMatchData = gameGetMatchData; diff --git a/src/lib/match/crescendo.ts b/src/lib/match/crescendo.ts new file mode 100644 index 0000000..d1f5df8 --- /dev/null +++ b/src/lib/match/crescendo.ts @@ -0,0 +1,120 @@ +import type { GoogleSpreadsheetRow } from "google-spreadsheet"; + +export interface Match { + matchNumber: number; + + red1: string; + red2: string; + red3: string; + blue1: string; + blue2: string; + blue3: string; + + redScore: number; + blueScore: number; + + redPenalty: number; + bluePenalty: number; + redAuto: number; + blueAuto: number; + redTeleop: number; + blueTeleop: number; + redEnd: number; + blueEnd: number; + + redGamePieces: number; + blueGamePieces: number; + + redRP: number; + blueRP: number; + redTiebreaker: number; + blueTiebreaker: number; + redBonusRP: number; + blueBonusRP: number; +} + +export function matchToArray(match: Match) { + return [ + match.matchNumber, + match.red1, + match.red2, + match.red3, + match.blue1, + match.blue2, + match.blue3, + match.redScore, + match.blueScore, + match.redPenalty, + match.bluePenalty, + match.redAuto, + match.blueAuto, + match.redTeleop, + match.blueTeleop, + match.redEnd, + match.blueEnd, + match.redGamePieces, + match.blueGamePieces, + match.redRP, + match.blueRP, + match.redTiebreaker, + match.blueTiebreaker, + match.redBonusRP, + match.blueBonusRP, + ]; +} + +export const headerValues = [ + "Match Number", + "Red 1", + "Red 2", + "Red 3", + "Blue 1", + "Blue 2", + "Blue 3", + "Red Score", + "Blue Score", + "Red Penalty", + "Blue Penalty", + "Red Auto", + "Blue Auto", + "Red Teleop", + "Blue Teleop", + "Red End", + "Blue End", + "Red Game Pieces", + "Blue Game Pieces", + "Red RP", + "Blue RP", + "Red Tiebreaker", + "Blue Tiebreaker", + "Red Bonus RP", + "Blue Bonus RP", +]; + +export function saveMatchToRow(match: Match, row: GoogleSpreadsheetRow) { + row["Match Number"] = match.matchNumber; + row["Red 1"] = match.red1; + row["Red 2"] = match.red2; + row["Red 3"] = match.red3; + row["Blue 1"] = match.blue1; + row["Blue 2"] = match.blue2; + row["Blue 3"] = match.blue3; + row["Red Score"] = match.redScore; + row["Blue Score"] = match.blueScore; + row["Red Penalty"] = match.redPenalty; + row["Blue Penalty"] = match.bluePenalty; + row["Red Auto"] = match.redAuto; + row["Blue Auto"] = match.blueAuto; + row["Red Teleop"] = match.redTeleop; + row["Blue Teleop"] = match.blueTeleop; + row["Red End"] = match.redEnd; + row["Blue End"] = match.blueEnd; + row["Red Game Pieces"] = match.redGamePieces; + row["Blue Game Pieces"] = match.blueGamePieces; + row["Red RP"] = match.redRP; + row["Blue RP"] = match.blueRP; + row["Red Tiebreaker"] = match.redTiebreaker; + row["Blue Tiebreaker"] = match.blueTiebreaker; + row["Red Bonus RP"] = match.redBonusRP; + row["Blue Bonus RP"] = match.blueBonusRP; +} diff --git a/src/lib/match/index.ts b/src/lib/match/index.ts index f2c77d2..0bd5641 100644 --- a/src/lib/match/index.ts +++ b/src/lib/match/index.ts @@ -1,3 +1,4 @@ +import type { GoogleSpreadsheetRow } from "google-spreadsheet"; import { type Match as chargedUpMatch, headerValues as chargedUpHeaderValues, @@ -5,20 +6,36 @@ import { saveMatchToRow as chargedUpSaveMatchToRow, } from "./chargedUp"; -let gameHeaderValues; -let gameMatchToArray; -let gameSaveMatchToRow; +import { + type Match as crescendoMatch, + headerValues as crescendoHeaderValues, + matchToArray as crescendoMatchToArray, + saveMatchToRow as crescendoSaveMatchToRow, +} from "./crescendo"; + +let gameHeaderValues: string[]; +let gameMatchToArray: (match: never) => (string | number)[]; +let gameSaveMatchToRow: (match: never, row: GoogleSpreadsheetRow) => void; switch (process.env.GAME_NAME) { case "CHARGED UP": - default: gameHeaderValues = chargedUpHeaderValues; gameMatchToArray = chargedUpMatchToArray; gameSaveMatchToRow = chargedUpSaveMatchToRow; break; + case "CRESCENDO": + default: + gameHeaderValues = crescendoHeaderValues; + gameMatchToArray = crescendoMatchToArray; + gameSaveMatchToRow = crescendoSaveMatchToRow; + break; } -export type Match = chargedUpMatch; // | otherMatch; +export type Match = chargedUpMatch | crescendoMatch; export const headerValues = gameHeaderValues; -export const matchToArray = gameMatchToArray; -export const saveMatchToRow = gameSaveMatchToRow; +export const matchToArray = (match: Match) => { + return gameMatchToArray(match as never); +}; +export const saveMatchToRow = (match: Match, row: GoogleSpreadsheetRow) => { + return gameSaveMatchToRow(match as never, row); +}; diff --git a/src/lib/resultEmbed/chargedUp.ts b/src/lib/resultEmbed/chargedUp.ts index d0383e1..9de4c4a 100644 --- a/src/lib/resultEmbed/chargedUp.ts +++ b/src/lib/resultEmbed/chargedUp.ts @@ -1,5 +1,5 @@ import { EmbedBuilder, type Guild } from "discord.js"; -import type { Match } from "../match"; +import type { Match } from "../match/chargedUp"; import { PLAYOFF_MATCHES_BEFORE_FINALS } from "../field"; const codeBlock = (str: string) => `\`\`\`\n${str}\n\`\`\``; diff --git a/src/lib/resultEmbed/crescendo.ts b/src/lib/resultEmbed/crescendo.ts new file mode 100644 index 0000000..f12f895 --- /dev/null +++ b/src/lib/resultEmbed/crescendo.ts @@ -0,0 +1,111 @@ +import { EmbedBuilder, type Guild } from "discord.js"; +import type { Match } from "../match/crescendo"; +import { PLAYOFF_MATCHES_BEFORE_FINALS } from "../field"; + +const codeBlock = (str: string) => `\`\`\`\n${str}\n\`\`\``; + +/** + * Sends an embed to the discord channel with the match results + * @param guild server to send the embed to + * @param match object containing match data + */ +async function sendMatchResultEmbed( + guild: Guild, + match: Match, + matchTitle: string +) { + // Get the channel to send the embed to + const channel = guild.channels.cache.get(process.env.DISCORD_CHANNEL_ID); + + const redAlliance = codeBlock( + [match.red1, match.red2, match.red3] + .map((x) => x.padEnd(10, " ")) + .join("\n") + ); + const blueAlliance = codeBlock( + [match.blue1, match.blue2, match.blue3] + .map((x) => x.padEnd(10, " ")) + .join("\n") + ); + + const breakdownTitle = "Match Breakdown"; + let redAllianceTitle = "Red Alliance :red_square:"; + let blueAllianceTitle = ":blue_square: Blue Alliance"; + let color = 0x888888; + + if (match.redScore > match.blueScore) { + redAllianceTitle = "Red Alliance :trophy:"; + color = 0xff0000; + } + + if (match.blueScore > match.redScore) { + blueAllianceTitle = ":trophy: Blue Alliance"; + color = 0x0000ff; + } + + const { + redAuto, + redTeleop, + redEnd, + redPenalty, + redGamePieces, + redRP, + blueAuto, + blueTeleop, + blueEnd, + bluePenalty, + blueGamePieces, + blueRP, + } = match; + + const breakdown = codeBlock( + [ + [redAuto, " | auto | ", blueAuto], + [redTeleop, " | teleop | ", blueTeleop], + [redEnd, " | endgame | ", blueEnd], + [redPenalty, " | penalties | ", bluePenalty], + [redGamePieces, " | game pieces | ", blueGamePieces], + ["", " | | ", ""], + [redRP, " | ranking points | ", blueRP], + ] + .map( + (x) => + x[0].toString().padStart(3, " ") + + x[1] + + x[2].toString().padEnd(3, " ") + ) + .join("\n") + ); + + const embed = new EmbedBuilder() + .setColor(color) + .setTitle( + `${matchTitle.padEnd(24, " ")} ${match.redScore + .toString() + .padEnd(3, " ")} - ${match.blueScore.toString().padEnd(3, " ")}` + ) + .addFields( + { name: redAllianceTitle, value: redAlliance, inline: true }, + { name: blueAllianceTitle, value: blueAlliance, inline: true }, + { name: breakdownTitle, value: breakdown, inline: false } + ) + .setTimestamp(); + + if (channel?.isTextBased()) { + await channel.send({ embeds: [embed] }); + } +} + +export async function sendQualMatchEmbed(guild: Guild, match: Match) { + await sendMatchResultEmbed(guild, match, `Qual ${match.matchNumber} Results`); +} + +export async function sendPlayoffMatchEmbed(guild: Guild, match: Match) { + await sendMatchResultEmbed( + guild, + match, + match.matchNumber > PLAYOFF_MATCHES_BEFORE_FINALS + ? `Finals ${match.matchNumber - PLAYOFF_MATCHES_BEFORE_FINALS} Results` + : `Playoff ${match.matchNumber} Results` + ); +} diff --git a/src/lib/resultEmbed/index.ts b/src/lib/resultEmbed/index.ts index 7b2c1f0..9a3c44b 100644 --- a/src/lib/resultEmbed/index.ts +++ b/src/lib/resultEmbed/index.ts @@ -1,10 +1,12 @@ +import type { Guild } from "discord.js"; import { sendQualMatchEmbed as chargedUpSendQualMatchEmbed, sendPlayoffMatchEmbed as chargedUpSendPlayoffMatchEmbed, } from "./chargedUp"; +import type { Match } from "../match"; -let gameSendQualMatchEmbed; -let gameSendPlayoffMatchEmbed; +let gameSendQualMatchEmbed: (guild: Guild, match: never) => void; +let gameSendPlayoffMatchEmbed: (guild: Guild, match: never) => void; switch (process.env.GAME_NAME) { case "CHARGED UP": @@ -14,5 +16,9 @@ switch (process.env.GAME_NAME) { break; } -export const sendQualMatchEmbed = gameSendQualMatchEmbed; -export const sendPlayoffMatchEmbed = gameSendPlayoffMatchEmbed; +export const sendQualMatchEmbed = (guild: Guild, match: Match) => { + return gameSendQualMatchEmbed(guild, match as never); +}; +export const sendPlayoffMatchEmbed = (guild: Guild, match: Match) => { + return gameSendPlayoffMatchEmbed(guild, match as never); +}; From 2f63963a22154f0081508b4b7905666ce8095338 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 9 Jun 2024 05:39:23 -0400 Subject: [PATCH 4/6] Refactor code to use environment variable for number of teams per alliance --- src/commands/generate_schedule.ts | 2 ++ src/lib/googleSheet.ts | 58 +++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/commands/generate_schedule.ts b/src/commands/generate_schedule.ts index 7020e88..b3f1ff7 100644 --- a/src/commands/generate_schedule.ts +++ b/src/commands/generate_schedule.ts @@ -75,6 +75,8 @@ export const execute = async (interaction: ChatInputCommandInteraction) => { String(rounds), quality, "-q", + "-a", + String(process.env.TEAMS_PER_ALLIANCE), ]); // Pipe the output of MatchMaker to a file diff --git a/src/lib/googleSheet.ts b/src/lib/googleSheet.ts index 0f63e35..abb1d14 100644 --- a/src/lib/googleSheet.ts +++ b/src/lib/googleSheet.ts @@ -185,12 +185,14 @@ export async function getMatchPlayers( const row = await getMatch(scheduleSheet, matchNumber); return [ - row["Red 1"], - row["Red 2"], - row["Red 3"], - row["Blue 1"], - row["Blue 2"], - row["Blue 3"], + ...Array.from( + { length: process.env.TEAMS_PER_ALLIANCE }, + (_, i) => row[`Red ${i + 1}`] + ), + ...Array.from( + { length: process.env.TEAMS_PER_ALLIANCE }, + (_, i) => row[`Blue ${i + 1}`] + ), ]; } @@ -219,20 +221,20 @@ export async function copyScheduleToMatchesSheet( if (!matchRow) { await matchesSheet.addRow([ matchNumber, - players.get(row["Red 1"]), - players.get(row["Red 2"]), - players.get(row["Red 3"]), - players.get(row["Blue 1"]), - players.get(row["Blue 2"]), - players.get(row["Blue 3"]), + players.get(row["Red 1"]) ?? "", + players.get(row["Red 2"]) ?? "", + players.get(row["Red 3"]) ?? "", + players.get(row["Blue 1"]) ?? "", + players.get(row["Blue 2"]) ?? "", + players.get(row["Blue 3"]) ?? "", ]); } else { - matchRow["Red 1"] = players.get(row["Red 1"]); - matchRow["Red 2"] = players.get(row["Red 2"]); - matchRow["Red 3"] = players.get(row["Red 3"]); - matchRow["Blue 1"] = players.get(row["Blue 1"]); - matchRow["Blue 2"] = players.get(row["Blue 2"]); - matchRow["Blue 3"] = players.get(row["Blue 3"]); + matchRow["Red 1"] = players.get(row["Red 1"]) ?? ""; + matchRow["Red 2"] = players.get(row["Red 2"]) ?? ""; + matchRow["Red 3"] = players.get(row["Red 3"]) ?? ""; + matchRow["Blue 1"] = players.get(row["Blue 1"]) ?? ""; + matchRow["Blue 2"] = players.get(row["Blue 2"]) ?? ""; + matchRow["Blue 3"] = players.get(row["Blue 3"]) ?? ""; await matchRow.save(); } } @@ -250,12 +252,10 @@ export async function postSchedule( for (const match of schedule) { const row = rows.find((r) => r["Match Number"] == match.number); if (row) { - row["Red 1"] = match.teams[0]; - row["Red 2"] = match.teams[1]; - row["Red 3"] = match.teams[2]; - row["Blue 1"] = match.teams[3]; - row["Blue 2"] = match.teams[4]; - row["Blue 3"] = match.teams[5]; + for (let i = 0; i < process.env.TEAMS_PER_ALLIANCE; i++) { + row[`Red ${i + 1}`] = match.teams[i]; + row[`Blue ${i + 1}`] = match.teams[i + process.env.TEAMS_PER_ALLIANCE]; + } row["Discord Ids"] = playerIds[i]; row["Display Names"] = playerNames[i]; @@ -263,12 +263,10 @@ export async function postSchedule( } else { await scheduleSheet.addRow([ match.number, - match.teams[0], - match.teams[1], - match.teams[2], - match.teams[3], - match.teams[4], - match.teams[5], + ...match.teams.slice(0, process.env.TEAMS_PER_ALLIANCE), + ...Array.from({ length: 3 - process.env.TEAMS_PER_ALLIANCE }, () => ""), + ...match.teams.slice(process.env.TEAMS_PER_ALLIANCE), + ...Array.from({ length: 3 - process.env.TEAMS_PER_ALLIANCE }, () => ""), "", playerIds[i], playerNames[i], From d1afdc47c44adf2fcf66452a57eda2d78aadc2e5 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 9 Jun 2024 19:27:32 -0400 Subject: [PATCH 5/6] Bump crescendo RP threshold --- src/lib/field/crescendo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/field/crescendo.ts b/src/lib/field/crescendo.ts index 1d698b3..16364ff 100644 --- a/src/lib/field/crescendo.ts +++ b/src/lib/field/crescendo.ts @@ -3,7 +3,7 @@ import fsSync from "fs"; import type { GoogleSpreadsheetRow } from "google-spreadsheet"; import type { Match } from "../match/crescendo"; -const MELODY_BONUS_RP = 25; +const MELODY_BONUS_RP = 30; const ENSEMBLE_BONUS_RP = 10; export async function getMatchData( From c986a94204258929779d9d92743868b5ecf82d3e Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 9 Jun 2024 19:28:29 -0400 Subject: [PATCH 6/6] Fix number env variable and crescendo result embed --- src/config/env.ts | 2 +- src/lib/resultEmbed/index.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index f980377..a8ada3a 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -13,7 +13,7 @@ const envSchema = z.object({ DISCORD_CHANNEL_ID: z.string().min(1), DISCORD_CATEGORY_ID: z.string().min(1), GAME_NAME: z.enum(["CHARGED UP", "CRESCENDO"]), - TEAMS_PER_ALLIANCE: z.number().int(), + TEAMS_PER_ALLIANCE: z.string().transform(Number), }); envSchema.parse(process.env); diff --git a/src/lib/resultEmbed/index.ts b/src/lib/resultEmbed/index.ts index 9a3c44b..1f98852 100644 --- a/src/lib/resultEmbed/index.ts +++ b/src/lib/resultEmbed/index.ts @@ -3,6 +3,11 @@ import { sendQualMatchEmbed as chargedUpSendQualMatchEmbed, sendPlayoffMatchEmbed as chargedUpSendPlayoffMatchEmbed, } from "./chargedUp"; +import { + sendQualMatchEmbed as crescendoSendQualMatchEmbed, + sendPlayoffMatchEmbed as crescendoSendPlayoffMatchEmbed, +} from "./crescendo"; + import type { Match } from "../match"; let gameSendQualMatchEmbed: (guild: Guild, match: never) => void; @@ -10,10 +15,14 @@ let gameSendPlayoffMatchEmbed: (guild: Guild, match: never) => void; switch (process.env.GAME_NAME) { case "CHARGED UP": - default: gameSendQualMatchEmbed = chargedUpSendQualMatchEmbed; gameSendPlayoffMatchEmbed = chargedUpSendPlayoffMatchEmbed; break; + case "CRESCENDO": + default: + gameSendQualMatchEmbed = crescendoSendQualMatchEmbed; + gameSendPlayoffMatchEmbed = crescendoSendPlayoffMatchEmbed; + break; } export const sendQualMatchEmbed = (guild: Guild, match: Match) => {