diff --git a/src/config/env.ts b/src/config/env.ts index a8ada3a..5b703f8 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", "CRESCENDO"]), + GAME_NAME: z.enum(["RAPID REACT", "CHARGED UP", "CRESCENDO"]), TEAMS_PER_ALLIANCE: z.string().transform(Number), }); diff --git a/src/lib/field/index.ts b/src/lib/field/index.ts index 2943f50..837500b 100644 --- a/src/lib/field/index.ts +++ b/src/lib/field/index.ts @@ -3,6 +3,7 @@ import fsSync from "fs"; import { getMatchData as chargedUpGetMatchData } from "./chargedUp"; import { getMatchData as crescendoGetMatchData } from "./crescendo"; +import { getMatchData as rapidReactGetMatchData } from "./rapidReact"; export const PLAYOFF_MATCHES_BEFORE_FINALS = 13; @@ -25,6 +26,9 @@ export async function setMatchNumber(matchType: string, matchNumber: number) { let gameGetMatchData; switch (process.env.GAME_NAME) { + case "RAPID REACT": + gameGetMatchData = rapidReactGetMatchData; + break; case "CHARGED UP": gameGetMatchData = chargedUpGetMatchData; break; diff --git a/src/lib/field/rapidReact.ts b/src/lib/field/rapidReact.ts new file mode 100644 index 0000000..b3a6b4a --- /dev/null +++ b/src/lib/field/rapidReact.ts @@ -0,0 +1,146 @@ +import fs from "fs/promises"; +import fsSync from "fs"; +import type { GoogleSpreadsheetRow } from "google-spreadsheet"; +import type { Match } from "../match/crescendo"; + +const CARGO_BONUS_RP = 60; +const HANGAR_BONUS_RP = 22; + +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 (cargo) + const piecesRed = + parseInt(await fs.readFile(`${dataDirectory}/C_H_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/C_L_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Auto_C_H_R.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Auto_C_L_R.txt`, "utf8")); + const piecesBlue = + parseInt(await fs.readFile(`${dataDirectory}/C_H_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/C_L_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Auto_C_H_B.txt`, "utf8")) + + parseInt(await fs.readFile(`${dataDirectory}/Auto_C_L_B.txt`, "utf8")); + + // Calculate endgame points + const endRed = parseInt( + await fs.readFile(`${dataDirectory}/EndR.txt`, "utf8") + ); + const endBlue = parseInt( + await fs.readFile(`${dataDirectory}/EndB.txt`, "utf8") + ); + + // Calculate auto points + const autoRed = parseInt( + await fs.readFile(`${dataDirectory}/AutoR.txt`, "utf8") + ); + const autoBlue = parseInt( + await fs.readFile(`${dataDirectory}/AutoB.txt`, "utf8") + ); + + // Calculate ranking points + const scoreRed = parseInt( + await fs.readFile(`${dataDirectory}/ScoreR.txt`, "utf8") + ); + const scoreBlue = parseInt( + await fs.readFile(`${dataDirectory}/ScoreB.txt`, "utf8") + ); + + const rpRedBonus = + (piecesRed >= CARGO_BONUS_RP ? 1 : 0) + (endRed >= HANGAR_BONUS_RP ? 1 : 0); + const rpRed = + rpRedBonus + (scoreRed > scoreBlue ? 2 : scoreRed === scoreBlue ? 1 : 0); + + const rpBlueBonus = + (piecesBlue >= CARGO_BONUS_RP ? 1 : 0) + + (endBlue >= HANGAR_BONUS_RP ? 1 : 0); + const rpBlue = + rpBlueBonus + (scoreBlue > scoreRed ? 2 : scoreBlue === scoreRed ? 1 : 0); + + // Calculate tiebreakers + const penaltyRed = parseInt( + await fs.readFile(`${dataDirectory}/PenR.txt`, "utf8") + ); + const penaltyBlue = parseInt( + await fs.readFile(`${dataDirectory}/PenB.txt`, "utf8") + ); + + 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}/TeleR.txt`, "utf8") + ), + blueTeleop: parseInt( + await fs.readFile(`${dataDirectory}/TeleB.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/match/index.ts b/src/lib/match/index.ts index 0bd5641..56ee1f8 100644 --- a/src/lib/match/index.ts +++ b/src/lib/match/index.ts @@ -13,11 +13,23 @@ import { saveMatchToRow as crescendoSaveMatchToRow, } from "./crescendo"; +import { + type Match as rapidReactMatch, + headerValues as rapidReactHeaderValues, + matchToArray as rapidReactMatchToArray, + saveMatchToRow as rapidReactSaveMatchToRow, +} from "./rapidReact"; + let gameHeaderValues: string[]; let gameMatchToArray: (match: never) => (string | number)[]; let gameSaveMatchToRow: (match: never, row: GoogleSpreadsheetRow) => void; switch (process.env.GAME_NAME) { + case "RAPID REACT": + gameHeaderValues = rapidReactHeaderValues; + gameMatchToArray = rapidReactMatchToArray; + gameSaveMatchToRow = rapidReactSaveMatchToRow; + break; case "CHARGED UP": gameHeaderValues = chargedUpHeaderValues; gameMatchToArray = chargedUpMatchToArray; @@ -31,7 +43,7 @@ switch (process.env.GAME_NAME) { break; } -export type Match = chargedUpMatch | crescendoMatch; +export type Match = chargedUpMatch | crescendoMatch | rapidReactMatch; export const headerValues = gameHeaderValues; export const matchToArray = (match: Match) => { return gameMatchToArray(match as never); diff --git a/src/lib/match/rapidReact.ts b/src/lib/match/rapidReact.ts new file mode 100644 index 0000000..d1f5df8 --- /dev/null +++ b/src/lib/match/rapidReact.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/resultEmbed/index.ts b/src/lib/resultEmbed/index.ts index 1f98852..a56d1c4 100644 --- a/src/lib/resultEmbed/index.ts +++ b/src/lib/resultEmbed/index.ts @@ -7,6 +7,10 @@ import { sendQualMatchEmbed as crescendoSendQualMatchEmbed, sendPlayoffMatchEmbed as crescendoSendPlayoffMatchEmbed, } from "./crescendo"; +import { + sendQualMatchEmbed as rapidReactSendQualMatchEmbed, + sendPlayoffMatchEmbed as rapidReactSendPlayoffMatchEmbed, +} from "./rapidReact"; import type { Match } from "../match"; @@ -14,6 +18,10 @@ let gameSendQualMatchEmbed: (guild: Guild, match: never) => void; let gameSendPlayoffMatchEmbed: (guild: Guild, match: never) => void; switch (process.env.GAME_NAME) { + case "RAPID REACT": + gameSendQualMatchEmbed = rapidReactSendQualMatchEmbed; + gameSendPlayoffMatchEmbed = rapidReactSendPlayoffMatchEmbed; + break; case "CHARGED UP": gameSendQualMatchEmbed = chargedUpSendQualMatchEmbed; gameSendPlayoffMatchEmbed = chargedUpSendPlayoffMatchEmbed; diff --git a/src/lib/resultEmbed/rapidReact.ts b/src/lib/resultEmbed/rapidReact.ts new file mode 100644 index 0000000..e82cef1 --- /dev/null +++ b/src/lib/resultEmbed/rapidReact.ts @@ -0,0 +1,111 @@ +import { EmbedBuilder, type Guild } from "discord.js"; +import type { Match } from "../match/rapidReact"; +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` + ); +}