Skip to content
30 changes: 22 additions & 8 deletions src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
);
if (defined.length === 0) return 0;
const total = defined.reduce(
(acc, cur) => acc + accuracyToPercentage[cur.accuracy as any],

Check warning on line 102 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
0,
);
return total / defined.length;
Expand Down Expand Up @@ -135,7 +135,7 @@
total += e.points ?? 0;
});
// Include auto climb points (15) when succeeded
const autoClimb = (sr as any).autoClimb as

Check warning on line 138 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
| "SUCCEEDED"
| "FAILED"
| "N_A"
Expand Down Expand Up @@ -200,7 +200,7 @@
const totalFuel = (r.events ?? [])
.filter((e) => e.action === "STOP_SCORING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
const durations = calculateTimeMetric([r] as any, "SCORING");

Check warning on line 203 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const duration = durations.reduce((a, b) => a + b, 0);
return duration > 0 ? totalFuel / duration : 0;
});
Expand All @@ -214,7 +214,7 @@
},
};
matchAggregationFunction = (reports) => {
const feedTime = calculateTimeMetric(reports as any, "FEEDING");

Check warning on line 217 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const feeds = reports.flatMap((r) =>
(r.events || []).filter((e) => e.action === "STOP_FEEDING"),
);
Expand All @@ -231,9 +231,9 @@
case Metric.timeFeeding:
srSelect = {
events: { select: { action: true, time: true } },
} as any;

Check warning on line 234 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
matchAggregationFunction = (reports) => {
const perMatch = calculateTimeMetric(reports as any, "FEEDING");

Check warning on line 236 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
return avg(perMatch);
};
break;
Expand All @@ -242,7 +242,7 @@
case Metric.totalDefenseTime:
srSelect = {
events: { select: { action: true, time: true } },
} as any;

Check warning on line 245 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
matchAggregationFunction = (reports) => {
const contact = avg(
calculateTimeMetric(reports as any, "DEFENDING"),
Expand Down Expand Up @@ -298,6 +298,22 @@
};
break;

case Metric.totalBallsFed:
srSelect = {
events: {
select: { action: true, quantity: true },
},
} as any;
matchAggregationFunction = (reports) => {
const perReportTotals = reports.map((r) => {
return (r.events ?? [])
.filter((e) => e.action === "STOP_FEEDING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
});
return avg(perReportTotals);
};
break;

case Metric.outpostIntakes:
srSelect = {
events: {
Expand Down Expand Up @@ -516,18 +532,16 @@

let matchValue = 0;
if (metric === Metric.fuelPerSecond) {
// Mirror averageManyFast: use total SCORING duration across all reports
// Total fuel across all reports / total duration across all reports
const totalFuel = (row.scoutReports as any)
.flatMap((r: any) => r.events ?? [])
.filter((e: any) => e.action === "STOP_SCORING")
.reduce((acc: number, cur: any) => acc + (cur.quantity ?? 0), 0);
const totalDuration = calculateTimeMetric(
row.scoutReports as any,
"SCORING",
).reduce((a, b) => a + b, 0);
const perReportRates = (row.scoutReports as any).map((r: any) => {
const totalFuel = (r.events ?? [])
.filter((e: any) => e.action === "STOP_SCORING")
.reduce((acc: number, cur: any) => acc + (cur.quantity ?? 0), 0);
return totalDuration > 0 ? totalFuel / totalDuration : 0;
});
matchValue = avg(perReportRates);
matchValue = totalDuration > 0 ? totalFuel / totalDuration : 0;
} else {
matchValue = matchAggregationFunction!(row.scoutReports as any);
}
Expand Down
149 changes: 81 additions & 68 deletions src/handler/analysis/coreAnalysis/averageManyFast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,25 @@ const config: AnalysisFunctionConfig<typeof argsSchema, z.ZodType> = {
const finalResults: Record<string, Record<string, number>> = {};

for (const metric of args.metrics) {
const resultsByTeam: Record<number, number[]> = {};
for (const team of args.teams) resultsByTeam[team] = [];
// Group by team -> tournament -> match values
const resultsByTeamAndTournament: Record<
number,
Record<string, number[]>
> = {};
for (const team of args.teams) resultsByTeamAndTournament[team] = {};

for (const row of tmd) {
if (!row.scoutReports.length) continue;

const team = row.teamNumber;
const tnmt = row.tournamentKey;
const sr = row.scoutReports;

let tournamentValue = 0;
if (!resultsByTeamAndTournament[team][tnmt]) {
resultsByTeamAndTournament[team][tnmt] = [];
}

let matchValue = 0;

switch (metric) {
case Metric.autoClimbStartTime: {
Expand All @@ -128,12 +137,15 @@ const config: AnalysisFunctionConfig<typeof argsSchema, z.ZodType> = {
);
});
const nonNullTimes = times.filter((t): t is number => t !== null);
if (nonNullTimes.length === 0) { tournamentValue = -1; break; }
if (nonNullTimes.length === 0) {
matchValue = -1;
break;
}
const adjustedTimes = nonNullTimes.map((t) => {
const remaining = autoEnd - t;
return remaining >= 0 ? remaining : 0;
});
tournamentValue = avg(adjustedTimes.length ? adjustedTimes : [0]);
matchValue = avg(adjustedTimes.length ? adjustedTimes : [0]);
break;
}

Expand All @@ -151,91 +163,88 @@ const config: AnalysisFunctionConfig<typeof argsSchema, z.ZodType> = {
if (r.endgameClimb !== required) return null;
return firstEventTime(
r.events,
(e) => e.action === "CLIMB" && e.time > autoEnd && e.time <= 158,
(e) =>
e.action === "CLIMB" && e.time > autoEnd && e.time <= 158,
);
});

const nonNullTimes = times.filter((t): t is number => t !== null);
if (nonNullTimes.length === 0) { tournamentValue = -1; break; }
if (nonNullTimes.length === 0) {
matchValue = -1;
break;
}
const adjustedTimes = nonNullTimes.map((t) => {
const remaining = 158 - t;
return remaining >= 0 ? remaining : 0;
});
tournamentValue = avg(adjustedTimes.length ? adjustedTimes : [0]);
matchValue = avg(adjustedTimes.length ? adjustedTimes : [0]);
break;
}
case Metric.contactDefenseTime:
case Metric.campingDefenseTime:
case Metric.totalDefenseTime: {
const perMatch = sr.map((r) => {
const contact = (tournamentValue = avg(
calculateTimeMetric(sr, "DEFENDING"),
));
const camping = (tournamentValue = avg(
calculateTimeMetric(sr, "CAMPING"),
));
if (metric === Metric.contactDefenseTime) return contact;
if (metric === Metric.campingDefenseTime) return camping;
return contact + camping;
});
tournamentValue = avg(perMatch);
const contact = avg(calculateTimeMetric(sr, "DEFENDING"));
const camping = avg(calculateTimeMetric(sr, "CAMPING"));
if (metric === Metric.contactDefenseTime) matchValue = contact;
else if (metric === Metric.campingDefenseTime) matchValue = camping;
else matchValue = contact + camping;
break;
}

/* ---------- SCORED METRICS ---------- */
case Metric.driverAbility:
tournamentValue = avg(sr.map((r) => r.driverAbility));
matchValue = avg(sr.map((r) => r.driverAbility));
break;
case Metric.defenseEffectiveness:
tournamentValue = avg(sr.map((r) => r.defenseEffectiveness));
matchValue = avg(sr.map((r) => r.defenseEffectiveness));
break;
case Metric.accuracy:
{
const defined = sr.filter((r) => r.accuracy !== null && r.accuracy !== undefined);
tournamentValue = defined.length
? avg(defined.map((r) => accuracyToPercentage[r.accuracy as any]))
const defined = sr.filter(
(r) => r.accuracy !== null && r.accuracy !== undefined,
);
matchValue = defined.length
? avg(
defined.map((r) => accuracyToPercentage[r.accuracy as any]),
)
: 0;
}
break;
case Metric.autoPoints:
case Metric.teleopPoints:
case Metric.totalPoints: {
const perMatch = sr.map((r) => {
const perReport = sr.map((r) => {
const auto = r.events
.filter((e) => e.time <= autoEnd && e.action === "STOP_SCORING")
.reduce((a, b) => a + b.points, 0);
const tele = r.events
.filter((e) => e.time > autoEnd && e.action === "STOP_SCORING")
.reduce((a, b) => a + b.points, 0);
const aClimb = avg(
sr.map((s) => (s.autoClimb !== AutoClimb.SUCCEEDED ? 0 : 15)),
);
const endgame = avg(
sr.map((s) => endgameToPoints[s.endgameClimb]),
);
if (metric === Metric.autoPoints) return auto;
if (metric === Metric.teleopPoints) return tele;
return aClimb + auto + tele + endgame;
const aClimb = r.autoClimb === AutoClimb.SUCCEEDED ? 15 : 0;
const endgame = endgameToPoints[r.endgameClimb];
if (metric === Metric.autoPoints)
return auto * r.accuracy + aClimb;
if (metric === Metric.teleopPoints) return tele * r.accuracy;
return aClimb + auto * r.accuracy + tele * r.accuracy + endgame;
});
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
break;
}
case Metric.fuelPerSecond: {
const perMatch = sr.map((r) => {
const totalFuel = r.events
.filter((e) => e.action === "STOP_SCORING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
const duration = calculateTimeMetric(sr, "SCORING").reduce(
(a, b) => a + b,
0,
);
return duration > 0 ? totalFuel / duration : 0;
});
tournamentValue = avg(perMatch);
// Total fuel across all reports / total duration across all reports
const totalFuel = sr
.flatMap((r) => r.events)
.filter((e) => e.action === "STOP_SCORING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
const totalDuration = calculateTimeMetric(sr, "SCORING").reduce(
(a, b) => a + b,
0,
);
matchValue = totalDuration > 0 ? totalFuel / totalDuration : 0;
break;
}
case Metric.totalFuelOutputted: {
const perMatch = sr.map((r) => {
const perReport = sr.map((r) => {
const shotQty = r.events
.filter((e) => e.action === "STOP_SCORING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
Expand All @@ -244,35 +253,32 @@ const config: AnalysisFunctionConfig<typeof argsSchema, z.ZodType> = {
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
return shotQty + feedQty;
});
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
break;
}
case Metric.totalBallsFed: {
const perMatch = sr.map((r) => {
const perReport = sr.map((r) => {
return r.events
.filter((e) => e.action === "STOP_FEEDING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
});
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
break;
}
case Metric.totalBallThroughput: {
const perMatch = sr.map((r) => {
const perReport = sr.map((r) => {
return r.events
.filter(
(e) =>
e.action === "STOP_FEEDING" || e.action === "STOP_SCORING",
)
.filter((e) => e.action === "STOP_SCORING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
});
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
break;
}
case Metric.feedsPerMatch: {
const perMatch = sr.map(
const perReport = sr.map(
(r) => r.events.filter((e) => e.action === "STOP_FEEDING").length,
);
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
break;
}
case Metric.feedingRate: {
Expand All @@ -284,43 +290,50 @@ const config: AnalysisFunctionConfig<typeof argsSchema, z.ZodType> = {
(acc, f) => acc + (f.quantity ?? 0),
0,
);
tournamentValue =
matchValue =
totalFeedQuantity > 0 ? totalFeedQuantity / avg(feedTime) : 0;
break;
}
case Metric.timeFeeding: {
tournamentValue = avg(calculateTimeMetric(sr, "FEEDING"));
matchValue = avg(calculateTimeMetric(sr, "FEEDING"));
break;
}

case Metric.outpostIntakes: {
const perMatch = sr.map(
const perReport = sr.map(
(r) =>
r.events.filter(
(e) => e.action === "INTAKE" && e.position === "OUTPOST",
).length,
);
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
break;
}
default: {
const perMatch = sr.map(
const perReport = sr.map(
(r) =>
r.events.filter((e) => e.action === metricToEvent[metric])
.length,
);
tournamentValue = avg(perMatch);
matchValue = avg(perReport);
}
}

resultsByTeam[team].push(tournamentValue);
resultsByTeamAndTournament[team][tnmt].push(matchValue);
}

finalResults[String(metric)] = {};
for (const team of args.teams) {
const teamResults = resultsByTeam[team];
// First average within each tournament, then apply weighted average across tournaments
const tournamentAverages: number[] = [];
for (const values of Object.values(resultsByTeamAndTournament[team])) {
const valid = values.filter((v) => v !== -1);
if (valid.length > 0) tournamentAverages.push(avg(valid));
}
finalResults[String(metric)][String(team)] =
teamResults.length > 0 ? weightedTourAvgLeft(teamResults) : -1;
tournamentAverages.length > 0
? weightedTourAvgLeft(tournamentAverages)
: -1;
}
}

Expand Down
Loading
Loading