Skip to content
Open
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
137 changes: 137 additions & 0 deletions apps/scouting/backend/src/routes/forecast-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// בס"ד

import { Router } from "express";
import { pipe } from "fp-ts/lib/function";
import { createTypeCheckingEndpointFlow } from "../middleware/verification";
import {
type ClimbLevel,
convertGeneralToAllianceData,
defaultAllianceData,
type Forecast,
forecastProps,
type ScoutingForm,
} from "@repo/scouting_types";
import { right } from "fp-ts/lib/Either";
import { getFormsCollection } from "./forms-router";
import { flatMap, fold, fromEither, map, tryCatch } from "fp-ts/lib/TaskEither";
import { StatusCodes } from "http-status-codes";
import { groupBy } from "fp-ts/lib/NonEmptyArray";
import { calculateAverage, mapObject } from "@repo/array-functions";
import { generalCalculateFuel } from "../fuel/fuel-general";
import { getAllBPS } from "./teams-router";
import { calcAverageGeneralFuelData } from "./general-router";
import { castItem } from "@repo/type-utils";

export const forecastRouter = Router();

const CLIMB_SCORE_VALUES = { L0: 0, L1: 10, L2: 20, L3: 30 };

const TELE_CLIMB_MULTIPLIER = 1;
const AUTO_CLIMB_MULTIPLIER = 1.5;

const calculateAverageClimbScore = (climbs: ClimbLevel[], isAuto: boolean) =>
calculateAverage(
climbs,
(climb) =>
CLIMB_SCORE_VALUES[climb] *
(isAuto ? AUTO_CLIMB_MULTIPLIER : TELE_CLIMB_MULTIPLIER),
);

const calculateAverageClimbsScore = (forms: ScoutingForm[]) => ({
auto: calculateAverageClimbScore(
forms.map((form) => form.auto.climb.level),
true,
),
tele: calculateAverageClimbScore(
forms.map((form) => form.tele.climb.level),
false,
),
});

forecastRouter.get("/", async (req, res) => {
await pipe(
req.query,
castItem,
right,
createTypeCheckingEndpointFlow(forecastProps, (error) => ({
status: StatusCodes.BAD_REQUEST,
reason: `Query Does Not match Specified Type: ${error}`,
})),
fromEither,
flatMap((query) =>
pipe(
getFormsCollection(),
map((collection) => ({ collection, query })),
),
),
flatMap(({ collection, query }) =>
tryCatch(
async () => ({
redAlliance: await collection
.find({
teamNumber: { $in: query.redAlliance },
})
.toArray(),
blueAlliance: await collection
.find({
teamNumber: { $in: query.blueAlliance },
})
.toArray(),
}),
(error) => ({
status: StatusCodes.INTERNAL_SERVER_ERROR,
reason: `Error getting teams from DB: ${error}`,
}),
),
),
map((alliancesForms) =>
mapObject(
alliancesForms,
groupBy((form: ScoutingForm) => form.teamNumber.toString()),
),
),
map((alliancesForms) =>
mapObject(alliancesForms, Object.values<ScoutingForm[]>),
),
map((alliancesTeamedForms) => ({
alliancesTeamedForms,
bps: getAllBPS(),
})),
map(({ alliancesTeamedForms, bps }) =>
mapObject(alliancesTeamedForms, (allianceTeamedForms) =>
allianceTeamedForms.map((teamForms) => ({
climb: calculateAverageClimbsScore(teamForms),
fuel: calcAverageGeneralFuelData(
teamForms.map((form) => generalCalculateFuel(form, bps)),
),
})),
),
),
map((alliancesTeamedData) =>
mapObject(alliancesTeamedData, (allianceTeamedData) =>
allianceTeamedData.map(convertGeneralToAllianceData).reduce(
(acc, curr) => ({
climb: {
auto: acc.climb.auto + curr.climb.auto,
tele: acc.climb.tele + curr.climb.tele,
},
fuel: {
auto: acc.fuel.auto + curr.fuel.auto,
tele: acc.fuel.tele + curr.fuel.tele,
fullGame: acc.fuel.fullGame + curr.fuel.fullGame,
},
}),
defaultAllianceData,
),
),
),
fold(
(error) => () =>
Promise.resolve(res.status(error.status).send(error.reason)),
(allianceData) => () =>
Promise.resolve(
res.status(StatusCodes.OK).json({ allianceData } satisfies Forecast),
),
),
)();
});
2 changes: 1 addition & 1 deletion apps/scouting/backend/src/routes/general-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface AccumulatedFuelData {

const ONE_ITEM_ARRAY = 1;

const calcAverageGeneralFuelData = (fuelData: GeneralFuelData[]) => {
export const calcAverageGeneralFuelData = (fuelData: GeneralFuelData[]): GeneralFuelData => {
if (fuelData.length === ONE_ITEM_ARRAY || isEmpty(fuelData)) {
return firstElement(fuelData);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/scouting/backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StatusCodes } from "http-status-codes";
import { tbaRouter } from "./tba";
import { gameRouter } from "./game-router";
import { formsRouter } from "./forms-router";
import { forecastRouter } from "./forecast-router";
import { teamsRouter } from "./teams-router";
import { generalRouter } from "./general-router";

Expand All @@ -12,6 +13,7 @@ export const apiRouter = Router();
apiRouter.use("/forms", formsRouter);
apiRouter.use("/tba", tbaRouter);
apiRouter.use("/game", gameRouter);
apiRouter.use("/forecast", forecastRouter);
apiRouter.use("/team",teamsRouter)
apiRouter.use("/general", generalRouter);

Expand Down
48 changes: 48 additions & 0 deletions packages/scouting_types/forecast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// בס"ד
import * as t from "io-ts";
import type { GeneralFuelData } from "../rebuilt";

export const forecastProps = t.type({
redAlliance: t.tuple([t.number, t.number, t.number]),
blueAlliance: t.tuple([t.number, t.number, t.number]),
});

interface ClimbAllianceData {
auto: number;
tele: number;
}
interface FuelAllianceData {
auto: number;
tele: number;
fullGame: number;
}

export interface AllianceData {
climb: ClimbAllianceData;
fuel: FuelAllianceData;
}
export const defaultAllianceData: AllianceData = {
climb: { auto: 0, tele: 0 },
fuel: { auto: 0, tele: 0, fullGame: 0 },
};

export interface Forecast {
allianceData: { redAlliance: AllianceData; blueAlliance: AllianceData };
}

const PASS_POINT_VALUE = 0.6;

export const convertGeneralToAllianceData = ({
climb,
fuel,
}: {
climb: ClimbAllianceData;
fuel: GeneralFuelData;
}): AllianceData => ({
climb,
fuel: {
auto: fuel.auto.scored + fuel.auto.passed * PASS_POINT_VALUE,
tele: fuel.tele.scored + fuel.tele.passed * PASS_POINT_VALUE,
fullGame: fuel.fullGame.scored + fuel.fullGame.passed * PASS_POINT_VALUE,
},
});
1 change: 1 addition & 0 deletions packages/scouting_types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// בס"ד
export * from "./rebuilt";
export * from "./forecast";

export * from "./tba";
export * from "./teams"
Expand Down
1 change: 1 addition & 0 deletions packages/scouting_types/rebuilt/scouting_form/Shift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Interval = t.TypeOf<typeof intervalCodec>;
export type SingleLevelTime = Partial<Record<ActiveClimbLevel, Interval>>;

export type Climb = ScoutingForm["auto" | "tele"]["climb"];
export type ClimbLevel = Climb["level"];

export type TeleClimb = t.TypeOf<typeof climbCodec>;
export type TeleClimbSide = TeleClimb["climbSide"];
Expand Down
Loading