From e1288d9f667b3a061ceb552a43b86be9073bd988 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Tue, 13 Aug 2024 14:28:35 -0400 Subject: [PATCH 1/4] Base changes for the FGC 2024 game. --- front-end/src/layouts/referee-layout.tsx | 5 +- lib/models/src/seasons/FeedingTheFuture.ts | 387 +++++++++++++++++++++ lib/models/src/seasons/index.ts | 4 +- 3 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 lib/models/src/seasons/FeedingTheFuture.ts diff --git a/front-end/src/layouts/referee-layout.tsx b/front-end/src/layouts/referee-layout.tsx index 13409f6e..fd578d20 100644 --- a/front-end/src/layouts/referee-layout.tsx +++ b/front-end/src/layouts/referee-layout.tsx @@ -5,7 +5,6 @@ import Box from '@mui/material/Box'; import Container from '@mui/material/Container'; import { appbarConfigAtom } from '@stores/recoil'; - interface Props { title?: string; titleLink?: string; @@ -18,7 +17,7 @@ export const RefereeLayout: FC = ({ titleLink, containerWidth, children -}: Props) => { +}: Props) => { const [, updateAppbarConfig] = useRecoilState(appbarConfigAtom); useEffect(() => { @@ -26,7 +25,7 @@ export const RefereeLayout: FC = ({ title, titleLink, showFullscreen: true - }); + }); }, []); return ( diff --git a/lib/models/src/seasons/FeedingTheFuture.ts b/lib/models/src/seasons/FeedingTheFuture.ts new file mode 100644 index 00000000..97531331 --- /dev/null +++ b/lib/models/src/seasons/FeedingTheFuture.ts @@ -0,0 +1,387 @@ +import { AllianceMember } from '../base/Alliance.js'; +import { Match, MatchDetailBase } from '../base/Match.js'; +import { Ranking } from '../base/Ranking.js'; +import { isNonNullObject, isNumber } from '../types.js'; +import { Season, SeasonFunctions } from './index.js'; + +/** + * Score Table + * Final score is ((WaterConserved + EnergyConserved + FoodProduced) * BalanceMultiplier) + FoodSecured + Coopertition + */ +export const ScoreTable = { + Conserved: 1, + FoodProduced: 2, + FoodSecured: 2, + BalanceMultiplier: (nBalanced: number) => 1 + nBalanced * 0.2, + Coopertition: (nBalanced: number) => + nBalanced < 5 ? 0 : nBalanced >= 6 ? 30 : 15, + Foul: 0.1 // Needs to be applied to other alliance to be calculated properly +}; + +/** + * Main season function declaration for the whole file. + */ +const functions: SeasonFunctions = { + calculateRankings, + calculatePlayoffsRankings, + calculateScore +}; + +export interface MatchDetails extends MatchDetailBase { + redResevoirConserved: number; + redNexusConserved: number; + redFoodProduced: number; + redFoodSecured: number; + redRobotOneBalanced: number; + redRobotTwoBalanced: number; + redRobotThreeBalanced: number; + blueResevoirConserved: number; + blueNexusConserved: number; + blueFoodProduced: number; + blueFoodSecured: number; + blueRobotOneBalanced: number; + blueRobotTwoBalanced: number; + blueRobotThreeBalanced: number; + coopertition: number; +} + +export const defaultMatchDetails: MatchDetails = { + eventKey: '', + id: -1, + tournamentKey: '', + redResevoirConserved: 0, + redNexusConserved: 0, + redFoodProduced: 0, + redFoodSecured: 0, + redRobotOneBalanced: 0, + redRobotTwoBalanced: 0, + redRobotThreeBalanced: 0, + blueResevoirConserved: 0, + blueNexusConserved: 0, + blueFoodProduced: 0, + blueFoodSecured: 0, + blueRobotOneBalanced: 0, + blueRobotTwoBalanced: 0, + blueRobotThreeBalanced: 0, + coopertition: 0 +}; + +export const isFeedingTheFutureDetails = (obj: unknown): obj is MatchDetails => + isNonNullObject(obj) && + isNumber(obj.redResevoirConserved) && + isNumber(obj.blueResevoirConserved); + +export interface SeasonRanking extends Ranking { + rankingScore: number; + highestScore: number; + foodSecuredPoints: number; +} + +export const FeedingTheFutureSeason: Season = { + key: 'fgc_2024', + program: 'fgc', + name: 'Feeding The Future', + defaultMatchDetails, + functions +}; + +/* Functions for calculating ranks. */ +function calculateRankings( + matches: Match[], + prevRankings: SeasonRanking[] +): SeasonRanking[] { + const rankingMap: Map = new Map(); + const scoresMap: Map = new Map(); + + // In this loop calculate basic W-L-T, as well as basic game information + for (const match of matches) { + if (!match.participants) break; + for (const participant of match.participants) { + if (!rankingMap.get(participant.teamKey)) { + rankingMap.set(participant.teamKey, { + eventKey: participant.eventKey, + tournamentKey: participant.tournamentKey, + foodSecuredPoints: 0, + losses: 0, + played: 0, + rank: 0, + rankChange: 0, + rankingScore: 0, + teamKey: participant.teamKey, + ties: 0, + wins: 0, + highestScore: 0 + }); + } + + if (!scoresMap.get(participant.teamKey)) { + scoresMap.set(participant.teamKey, []); + } + + if ( + !isFeedingTheFutureDetails(match.details) || + participant.disqualified === 1 || + participant.surrogate > 0 + ) { + continue; + } + + const ranking = { + ...(rankingMap.get(participant.teamKey) as SeasonRanking) + }; + const scores = scoresMap.get(participant.teamKey) as number[]; + const redWin = match.redScore > match.blueScore; + const blueWin = match.blueScore > match.redScore; + const isTie = match.redScore === match.blueScore; + + if (participant.station < 20) { + // Red Alliance + if (participant.cardStatus === 2 || participant.noShow === 1) { + scoresMap.set(participant.teamKey, [...scores, 0]); + ranking.losses = ranking.losses + 1; + } else { + scoresMap.set(participant.teamKey, [...scores, match.redScore]); + ranking.wins = ranking.wins + (redWin ? 1 : 0); + ranking.losses = ranking.losses + (redWin ? 0 : 1); + ranking.ties = ranking.ties + (isTie ? 1 : 0); + ranking.foodSecuredPoints = + ranking.foodSecuredPoints + + match.details.redFoodSecured * ScoreTable.FoodSecured; + if (ranking.highestScore < match.redScore) { + ranking.highestScore = match.redScore; + } + } + } + + if (participant.station >= 20) { + // Blue Alliance + if (participant.cardStatus === 2 || participant.noShow === 1) { + scoresMap.set(participant.teamKey, [...scores, 0]); + ranking.losses = ranking.losses + 1; + } else { + scoresMap.set(participant.teamKey, [...scores, match.blueScore]); + ranking.wins = ranking.wins + (blueWin ? 1 : 0); + ranking.losses = ranking.losses + (blueWin ? 0 : 1); + ranking.ties = ranking.ties + (isTie ? 1 : 0); + ranking.foodSecuredPoints = + ranking.foodSecuredPoints + + match.details.blueFoodSecured * ScoreTable.FoodSecured; + if (ranking.highestScore < match.blueScore) { + ranking.highestScore = match.blueScore; + } + } + } + ranking.played = ranking.played + 1; + rankingMap.set(participant.teamKey, ranking); + } + } + + // In this loop, calculate ranking score + for (const key of rankingMap.keys()) { + const scores = scoresMap.get(key); + if (!scores) continue; + const ranking = { + ...rankingMap.get(key) + } as SeasonRanking; + + const lowestScore = ranking.played > 0 ? Math.min(...scores) : 0; + const index = scores.findIndex((s) => s === lowestScore); + const newScores = + scores.length > 1 + ? [...scores.slice(0, index), ...scores.slice(index + 1)] + : scores; + if (newScores.length > 0) { + ranking.rankingScore = Number( + ( + newScores.reduce((prev, curr) => prev + curr) / newScores.length + ).toFixed(2) + ); + } else { + ranking.rankingScore = 0; + } + rankingMap.set(key, ranking); + } + + // In this loop calculate team rankings + const rankings = [...rankingMap.values()].sort(compareRankings); + // In this loop calculate the rank change + for (let i = 0; i < rankings.length; i++) { + const prevRanking = prevRankings.find( + (r) => r.teamKey === rankings[i].teamKey + ); + rankings[i].rank = i + 1; + if (prevRanking) { + const rankDelta = + prevRanking.rank === 0 ? 0 : prevRanking.rank - rankings[i].rank; + rankings[i].rankChange = rankDelta; + rankings[i].eventKey = prevRanking.eventKey; + rankings[i].tournamentKey = prevRanking.tournamentKey; + } + } + + return rankings; +} + +export function calculatePlayoffsRankings( + matches: Match[], + prevRankings: SeasonRanking[], + members: AllianceMember[] +) { + const rankingMap: Map = new Map(); + + for (const match of matches) { + if (!match.participants) break; + for (const participant of match.participants) { + if (!rankingMap.get(participant.teamKey)) { + rankingMap.set(participant.teamKey, { + eventKey: participant.eventKey, + tournamentKey: participant.tournamentKey, + foodSecuredPoints: 0, + losses: 0, + played: 0, + rank: 0, + rankChange: 0, + rankingScore: 0, + teamKey: participant.teamKey, + ties: 0, + wins: 0, + highestScore: 0 + }); + } + + if (!isFeedingTheFutureDetails(match.details)) continue; + + const ranking = { + ...(rankingMap.get(participant.teamKey) as SeasonRanking) + }; + + if (participant.cardStatus === 2) { + ranking.played += 1; + rankingMap.set(participant.teamKey, ranking); + continue; + } + + if (participant.station < 20) { + ranking.rankingScore += match.redScore; + } else if (participant.station >= 20) { + ranking.rankingScore += match.blueScore; + } + + rankingMap.set(participant.teamKey, ranking); + } + } + + const rankings = [...rankingMap.values()].sort( + (a, b) => b.rankingScore - a.rankingScore + ); + const rankedMembers = [...rankings].map((r) => + members.find((m) => m.teamKey === r.teamKey) + ); + + const allianceRankMap: Map = new Map(); + let allianceRank = 1; + rankedMembers.forEach((m) => { + if (m?.isCaptain) { + allianceRankMap.set(m.allianceRank, allianceRank); + allianceRank += 1; + } + }); + + for (let i = 0; i < rankings.length; i++) { + const member = rankedMembers[i]; + const prevRanking = prevRankings.find( + (r) => r.teamKey === rankings[i].teamKey + ); + if (prevRanking && member) { + rankings[i].rank = allianceRankMap.get(member.allianceRank) || 0; + const rankDelta = + prevRanking.rank === 0 ? 0 : prevRanking.rank - rankings[i].rank; + rankings[i].rankChange = rankDelta; + rankings[i].eventKey = prevRanking.eventKey; + rankings[i].tournamentKey = prevRanking.tournamentKey; + } + } + return rankings; +} + +export function calculateScore(match: Match): [number, number] { + const { details } = match; + if (!details) return [0, 0]; + const nBalanced = getBalancedRobots(details); + const [redResevoirPoints, blueResevoirPoints] = getResevoirPoints(details); + const [redNexusPoints, blueNexusPoints] = getNexusPoints(details); + const [redFoodProduced, blueFoodProduced] = getFoodProducedPoints(details); + const [redFoodSecuredPoints, blueFoodSecuredPoints] = + getFoodSecuredPoints(details); + const coopertitionPoints = getCoopertitionPoints(details); + const redScore = + (redResevoirPoints + redNexusPoints + redFoodProduced) * + ScoreTable.BalanceMultiplier(nBalanced) + + redFoodSecuredPoints + + coopertitionPoints; + const blueScore = + (blueResevoirPoints + blueNexusPoints + blueFoodProduced) * + ScoreTable.BalanceMultiplier(nBalanced) + + blueFoodSecuredPoints + + coopertitionPoints; + const redPenalty = Math.round(match.redMinPen * ScoreTable.Foul * redScore); + const bluePenalty = Math.round( + match.blueMinPen * ScoreTable.Foul * blueScore + ); + return [redScore + bluePenalty, blueScore + redPenalty]; +} + +export function getResevoirPoints(details: MatchDetails): [number, number] { + return [ + details.redResevoirConserved * ScoreTable.Conserved, + details.blueResevoirConserved * ScoreTable.Conserved + ]; +} + +export function getNexusPoints(details: MatchDetails): [number, number] { + return [ + details.redNexusConserved * ScoreTable.Conserved, + details.blueNexusConserved * ScoreTable.Conserved + ]; +} + +export function getFoodProducedPoints(details: MatchDetails): [number, number] { + return [ + details.redFoodProduced * ScoreTable.FoodProduced, + details.blueFoodProduced * ScoreTable.FoodProduced + ]; +} + +export function getFoodSecuredPoints(details: MatchDetails): [number, number] { + return [ + details.redFoodSecured * ScoreTable.FoodSecured, + details.blueFoodSecured * ScoreTable.FoodSecured + ]; +} + +export function getBalancedRobots(details: MatchDetails): number { + return ( + details.redRobotOneBalanced + + details.redRobotTwoBalanced + + details.redRobotThreeBalanced + + details.blueRobotOneBalanced + + details.blueRobotTwoBalanced + + details.blueRobotThreeBalanced + ); +} + +export function getCoopertitionPoints(details: MatchDetails): number { + return ScoreTable.Coopertition(getBalancedRobots(details)); +} + +function compareRankings(a: SeasonRanking, b: SeasonRanking): number { + if (a.rankingScore !== b.rankingScore) { + return b.rankingScore - a.rankingScore; + } else if (a.highestScore !== b.highestScore) { + return b.highestScore - a.highestScore; + } else if (a.foodSecuredPoints !== b.foodSecuredPoints) { + return b.foodSecuredPoints - a.foodSecuredPoints; + } else { + return 0; + } +} diff --git a/lib/models/src/seasons/index.ts b/lib/models/src/seasons/index.ts index bd7de53e..3a855060 100644 --- a/lib/models/src/seasons/index.ts +++ b/lib/models/src/seasons/index.ts @@ -4,6 +4,7 @@ import { Ranking } from '../base/Ranking.js'; import { CarbonCaptureSeason } from './CarbonCapture.js'; import { ChargedUpSeason } from './ChargedUp.js'; import { CrescendoSeason } from './Crescendo.js'; +import { FeedingTheFutureSeason } from './FeedingTheFuture.js'; import { HydrogenHorizonsSeason } from './HydrogenHorizons.js'; export * from './CarbonCapture.js'; @@ -23,7 +24,8 @@ export const Seasons: Season[] = [ CarbonCaptureSeason, ChargedUpSeason, HydrogenHorizonsSeason, - CrescendoSeason + CrescendoSeason, + FeedingTheFutureSeason ]; export interface SeasonFunctions { From 2a7ee495d479ff20aace7b687127e27f3fd4c032 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Tue, 13 Aug 2024 23:26:57 -0400 Subject: [PATCH 2/4] Card status enums --- lib/models/src/seasons/FeedingTheFuture.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/models/src/seasons/FeedingTheFuture.ts b/lib/models/src/seasons/FeedingTheFuture.ts index 97531331..270b70e4 100644 --- a/lib/models/src/seasons/FeedingTheFuture.ts +++ b/lib/models/src/seasons/FeedingTheFuture.ts @@ -4,6 +4,13 @@ import { Ranking } from '../base/Ranking.js'; import { isNonNullObject, isNumber } from '../types.js'; import { Season, SeasonFunctions } from './index.js'; +export enum CardStatus { + WHITE_CARD = 3, + RED_CARD = 2, + YELLOW_CARD = 1, + NO_CARD = 0 +} + /** * Score Table * Final score is ((WaterConserved + EnergyConserved + FoodProduced) * BalanceMultiplier) + FoodSecured + Coopertition @@ -136,7 +143,10 @@ function calculateRankings( if (participant.station < 20) { // Red Alliance - if (participant.cardStatus === 2 || participant.noShow === 1) { + if ( + participant.cardStatus === CardStatus.RED_CARD || + participant.noShow === CardStatus.YELLOW_CARD + ) { scoresMap.set(participant.teamKey, [...scores, 0]); ranking.losses = ranking.losses + 1; } else { @@ -155,7 +165,10 @@ function calculateRankings( if (participant.station >= 20) { // Blue Alliance - if (participant.cardStatus === 2 || participant.noShow === 1) { + if ( + participant.cardStatus === CardStatus.RED_CARD || + participant.noShow === CardStatus.YELLOW_CARD + ) { scoresMap.set(participant.teamKey, [...scores, 0]); ranking.losses = ranking.losses + 1; } else { @@ -255,7 +268,7 @@ export function calculatePlayoffsRankings( ...(rankingMap.get(participant.teamKey) as SeasonRanking) }; - if (participant.cardStatus === 2) { + if (participant.cardStatus === CardStatus.RED_CARD) { ranking.played += 1; rankingMap.set(participant.teamKey, ranking); continue; From 69e3b85ed29a3dd1dd52c7b23865ee5ae0d4d903 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Tue, 13 Aug 2024 23:33:29 -0400 Subject: [PATCH 3/4] Code cleanup --- lib/models/src/seasons/FeedingTheFuture.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/models/src/seasons/FeedingTheFuture.ts b/lib/models/src/seasons/FeedingTheFuture.ts index 270b70e4..2e9d4751 100644 --- a/lib/models/src/seasons/FeedingTheFuture.ts +++ b/lib/models/src/seasons/FeedingTheFuture.ts @@ -154,8 +154,7 @@ function calculateRankings( ranking.wins = ranking.wins + (redWin ? 1 : 0); ranking.losses = ranking.losses + (redWin ? 0 : 1); ranking.ties = ranking.ties + (isTie ? 1 : 0); - ranking.foodSecuredPoints = - ranking.foodSecuredPoints + + ranking.foodSecuredPoints += match.details.redFoodSecured * ScoreTable.FoodSecured; if (ranking.highestScore < match.redScore) { ranking.highestScore = match.redScore; @@ -176,8 +175,7 @@ function calculateRankings( ranking.wins = ranking.wins + (blueWin ? 1 : 0); ranking.losses = ranking.losses + (blueWin ? 0 : 1); ranking.ties = ranking.ties + (isTie ? 1 : 0); - ranking.foodSecuredPoints = - ranking.foodSecuredPoints + + ranking.foodSecuredPoints += match.details.blueFoodSecured * ScoreTable.FoodSecured; if (ranking.highestScore < match.blueScore) { ranking.highestScore = match.blueScore; From f9ae2e048355aad0d9251399d3623b971ada05f7 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Tue, 13 Aug 2024 23:38:38 -0400 Subject: [PATCH 4/4] Code cleanup --- lib/models/src/seasons/FeedingTheFuture.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/models/src/seasons/FeedingTheFuture.ts b/lib/models/src/seasons/FeedingTheFuture.ts index 2e9d4751..028866d5 100644 --- a/lib/models/src/seasons/FeedingTheFuture.ts +++ b/lib/models/src/seasons/FeedingTheFuture.ts @@ -197,10 +197,7 @@ function calculateRankings( const lowestScore = ranking.played > 0 ? Math.min(...scores) : 0; const index = scores.findIndex((s) => s === lowestScore); - const newScores = - scores.length > 1 - ? [...scores.slice(0, index), ...scores.slice(index + 1)] - : scores; + const newScores = scores.length > 1 ? scores.splice(index, 1) : scores; if (newScores.length > 0) { ranking.rankingScore = Number( (