Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-game support #15

Merged
merged 6 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading