From 8f93f296838b9cd1f694663396bb440318f62719 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Sun, 9 Jun 2024 05:24:34 -0400 Subject: [PATCH] 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); +};