Skip to content

Commit

Permalink
Add multi-game support (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
NicholasBottone authored Jun 9, 2024
1 parent 8316e07 commit e71d2c5
Show file tree
Hide file tree
Showing 16 changed files with 541 additions and 55 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ DISCORD_CLIENT_ID=
DISCORD_GUILD_ID=
DISCORD_CHANNEL_ID=
DISCORD_CATEGORY_ID=

GAME_NAME=
TEAMS_PER_ALLIANCE=
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions src/commands/generate_schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "CRESCENDO"]),
TEAMS_PER_ALLIANCE: z.string().transform(Number),
});

envSchema.parse(process.env);
Expand Down
3 changes: 3 additions & 0 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};
24 changes: 3 additions & 21 deletions src/lib/field.ts → src/lib/field/chargedUp.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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}`
);
}
151 changes: 151 additions & 0 deletions src/lib/field/crescendo.ts
Original file line number Diff line number Diff line change
@@ -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 = 30;
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;
}
37 changes: 37 additions & 0 deletions src/lib/field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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;

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":
gameGetMatchData = chargedUpGetMatchData;
break;
case "CRESCENDO":
default:
gameGetMatchData = crescendoGetMatchData;
break;
}

export const getMatchData = gameGetMatchData;
59 changes: 29 additions & 30 deletions src/lib/googleSheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export async function setupConnection() {
alliancesSheet,
playoffScheduleSheet,
playoffMatchesSheet,
sheetTitle: doc.title,
};
}

Expand Down Expand Up @@ -184,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}`]
),
];
}

Expand Down Expand Up @@ -218,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();
}
}
Expand All @@ -249,25 +252,21 @@ 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];

await row.save();
} 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],
Expand Down
File renamed without changes.
Loading

0 comments on commit e71d2c5

Please sign in to comment.