/**
* @class Match
* @param {string} team1 Team number of the first team (red)
* @param {string} team2 Team number of the second team (red)
* @param {string} team3 Team number of the third team (red)
* @param {string} team4 Team number of the fourth team (blue)
* @param {string} team5 Team number of the fifth team (blue)
* @param {string} team6 Team number of the sixth team (blue)
*/
class Match {
/**
* @constructor
* @param {string} team1 Team number of the first team (red)
* @param {string} team2 Team number of the second team (red)
* @param {string} team3 Team number of the third team (red)
* @param {string} team4 Team number of the fourth team (blue)
* @param {string} team5 Team number of the fifth team (blue)
* @param {string} team6 Team number of the sixth team (blue)
*/
constructor(team1, team2, team3, team4, team5, team6) {
this.archive = false;
this.redAlliance = new Alliance(this, RED, team1, team2, team3);
this.blueAlliance = new Alliance(this, BLUE, team4, team5, team6);
this.events = [];
this.mode = {
auto: true,
teleop: false,
isAuto() {
return this.auto;
},
isTeleop() {
return !this.auto;
},
};
/**
* @object Timer
* @property {Match} match - The match that the timer is in
* @property {number} time - The current time of the timer
* @property {boolean} running - Whether the timer is running or not
* @property {number} autoLength - The length of the auto period in seconds
* @property {number} matchLength - The length of the match in seconds
* @memberof Match
*/
this.timer = {
match: this,
time: 0,
// Whether the timer is running or not
running: false,
autoLength: 15, // seconds
matchLength: 135, // 2:15 in seconds
// whether we are in auto or teleop
lastUpdate: Date.now(),
deltaTime: 0,
/**
* @method reset
* @description Reset the timer to 0
* @memberof Timer
*/
reset() {
this.time = 0;
this.running = false;
},
/**
* @method play
* @description Start the timer
* @memberof Timer
*/
play() {
if (this.time < this.matchLength) this.running = true;
},
/**
* @method pause
* @description Pause the timer
* @memberof Timer
*/
pause() {
this.running = false;
},
/**
* @method toggle
* @description Toggle the timer
* @memberof Timer
*/
toggle() {
if (this.time < this.matchLength) this.running = !this.running;
},
/**
* @method setTime
* @description Set the time of the timer
* @param {number} time The time to set the timer to
* @memberof Timer
*/
setTime(time) {
this.time = time;
},
/**
* @method delta
* @description Calculate the delta time
* @memberof Timer
* @returns {number} The delta time
*/
delta() {
const now = Date.now();
this.deltaTime = (now - this.lastUpdate) / 1000;
this.lastUpdate = now;
return this.deltaTime;
},
/**
* @method update
* @description Update the timer
* @memberof Timer
*/
update() {
// update the timer
this.delta(); // calculate the delta time
if (this.running) {
// increment the timer
this.time += this.deltaTime;
}
if (this.time > this.matchLength) {
// set the timer to the max time
this.time = this.matchLength;
this.running = false;
}
// update the game state
if (this.time > this.autoLength) {
// change the game state to teleop
this.match.mode.auto = false;
} else {
// change the game state to auto
this.match.mode.auto = true;
}
// update the timer on the user interface
UserInterface.updateTimer(this.toString());
// update the play/pause button
UserInterface.playing(this.running);
// update "Mobility Bonus" button's status
UserInterface.updateMobilityBonus(this.match.mode.isAuto());
// update UI for inventories
UserInterface.updateRobotInventories();
},
/**
* @method toString
* @description Convert the timer to a string
* @memberof Timer
* @returns {string} The timer as a string
*/
toString() {
// Calculate minutes from time
const minutes = Math.floor(this.time / 60);
// Calculate seconds from time
const seconds = Math.floor(this.time % 60);
// Calculate milliseconds from time
const milliseconds = Math.floor((this.time % 1) * 100);
// Return a formatted string
return `${minutes.toString()}:${seconds
.toString()
.padStart(2, "0")}.${milliseconds.toString().padStart(2, "0")}`;
},
};
/**
* @object Scoring
* @typedef {Object} Scoring
* @memberof Match
*/
this.scoring = {
/**
* @method fieldState
* @description Get the current state of the field
* @memberof Scoring
* @returns {Object} The current state of the field, which includes the scoring grid and the score
*/
getFieldState(events) {
// Create an object that represents the scoring grid
const scoringGrid = {
red: new Array(27).fill([]), // 9x3 grid
blue: new Array(27).fill([]), // 9x3 grid
};
// Create an object to keep track of the score for each alliance
const score = {
red: 0,
blue: 0,
};
// Loop through each event
for (const event of events) {
// If the event is a placeGamePiece event
if (event.type === "placeGamePiece") {
const { alliance, location, auto } = event;
// Location is the index of the array
scoringGrid[alliance][location].push(event.matchPiece);
// Row is the top, middle, or bottom
const row = this.getPieceRow(location);
// autoOrTeleop is "AUTO" or "TELEOP" depending on the time
const autoOrTeleop = auto ? "AUTO" : "TELEOP";
// Add the points to the score
score[alliance] += POINT_VALUES[autoOrTeleop].match_PIECES[row];
}
// If the event is a mobilityBonus event
if (event.type === "mobilityBonus") {
const { alliance, auto } = event;
if (auto) {
score[alliance] += POINT_VALUES.AUTO.MOBILITY;
} else {
throw new Error("Mobility bonus can only be awarded in auto");
}
}
}
// Calculate the link bonus for each alliance
score.red += this.calculateLinkBonus(scoringGrid.red);
score.blue += this.calculateLinkBonus(scoringGrid.blue);
return {
scoringGrid,
score,
};
},
/**
* @method calculateLinkBonus
* @description Calculate the link bonus for an alliance
* @memberof Scoring
* @param {Array} scoringGrid The scoring grid for an alliance
* @returns {number} The link bonus for the alliance
*/
calculateLinkBonus(scoringGrid) {
let score = 0;
let count = 0;
let iteration = 0;
for (const gamePiece of scoringGrid) {
// Reset on new row
if (iteration % 9 === 0) {
count = 0;
}
// If the game piece is not empty
if (gamePiece !== EMPTY) {
count++;
} else {
count = 0;
}
// If we have 3 in a row
if (count === 3) {
// Add the link bonus
score += POINT_VALUES.TELEOP.LINK;
// Reset the count
count = 0;
}
iteration++;
}
return score;
},
/**
* @method getPieceRow
* @description Get the row of a piece
* @memberof Scoring
* @param {number} piece The piece to get the row of
* @returns {string} The row of the piece
*/
getPieceRow(piece) {
if (piece < 9 * (TOP + 1)) {
return "TOP";
} else if (piece < 9 * (MIDDLE + 1)) {
return "MIDDLE";
}
return "BOTTOM";
},
};
}
/**
* @method start
* @description Start the match
* @memberof Match
*/
start() {
this.timer.start();
}
/**
* @method reset
* @description Reset the match
* @memberof Match
*/
reset() {
this.timer.reset();
if (!this.archive) {
this.events = [];
this.redAlliance.reset();
this.blueAlliance.reset();
}
}
/**
* @method update
* @description Update the match
* @memberof Match
*/
update() {
// update the timer element
this.timer.update();
UserInterface.updateGameState(this.mode.isTeleop() ? "TELEOP" : "AUTO");
}
/**
* @method setupTeamButtons
* @description Set up the team buttons
* @memberof Match
*/
setupTeamButtons() {
for (let color of ["red", "blue"]) {
for (let i = 1; i <= 3; i++) {
let btn = document.querySelector(
`button[data-alliance=${color}][data-team-index="${i}"]`
);
let alliance = color === "red" ? this.redAlliance : this.blueAlliance;
let team = alliance.robots[i - 1].team;
btn.innerHTML = team;
btn.setAttribute("team-number", team);
}
}
}
/**
* @method getRobot
* @description Gets a robot
* @memberof Match
* @param {object} robot robot data
* @returns {Robot | null}
*/
getRobot(robot) {
if (!robot) return null;
let alliance = robot.color === RED ? this.redAlliance : this.blueAlliance;
let team = alliance.robots.find((x) => x.team === robot.teamNumber);
return team;
}
/**
* @method serialize
* @description Converts the match to JSON
* @memberof Match
* @returns {string}
*/
serialize() {
let obj = {
archive: this.archive,
};
obj.events = this.events.map((x) => ({
robot: x.robot.team,
eventType: x.eventType,
}));
obj.redAlliance = {
color: this.redAlliance.color,
robots: this.redAlliance.robots.map((x) => ({
team: x.team,
startingPosition: x.startingPosition,
})),
};
obj.blueAlliance = {
color: this.blueAlliance.color,
robots: this.blueAlliance.robots.map((x) => ({
team: x.team,
startingPosition: x.startingPosition,
})),
};
return JSON.stringify(obj);
}
/**
* @function deserialize
* @description Converts a JSON representation of a match to the match itself
* @memberof Match
* @static
* @param {string} jsonRepresentation the JSON representation
* @returns {Match}
*/
static deserialize(jsonRepresentation) {
let obj = JSON.parse(jsonRepresentation);
let teamNumbers = obj.redAlliance.robots
.concat(obj.blueAlliance.robots)
.map((x) => x.team);
let match = new Match(...teamNumbers);
for (let event of obj.events) {
let color = obj.redAlliance.robots
.map((x) => x.team)
.includes(event.robot)
? RED
: BLUE;
match.events.push(
new Event(
match,
match.getRobot({ color: color, teamNumber: event.robot }),
event.eventType
)
);
}
return match;
}
}