From 7b516689ab58a3d82cb526475e9b13dd8c3edf90 Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 15:21:22 +0200 Subject: [PATCH 01/12] Yoni - better stuff --- .../src/fuel/calculations/fuel-averaging.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index a545482..5e4044f 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -67,6 +67,15 @@ const correctSectionToTimeFromEnd = (sections: number[]) => { .map((timestamp) => endTimestamp - timestamp); }; +const calculateFullSectionsBallAmount = ( + sections: number[][], + shotLength: number, +) => + calculateBallAmount( + sections.map(correctSectionToTimeFromEnd).sort(compareSections), + shotLength, + ); + export const calculateFuelByAveraging = ( shot: ShootEvent, match: Match, @@ -74,19 +83,13 @@ export const calculateFuelByAveraging = ( ): FuelObject => { const shotLength = shot.interval.end - shot.interval.start; - const scoredAmount = calculateBallAmount( - sections - .map((section) => section.score) - .map(correctSectionToTimeFromEnd) - .sort(compareSections), + const scoredAmount = calculateFullSectionsBallAmount( + sections.map((section) => section.score), shotLength, ); - const shotAmount = calculateBallAmount( - sections - .map((section) => section.shoot) - .map(correctSectionToTimeFromEnd) - .sort(compareSections), + const shotAmount = calculateFullSectionsBallAmount( + sections.map((section) => section.shoot), shotLength, ); From 2c142a488a55aa324b15bc895cc9ad5b8532c334 Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 16:11:08 +0200 Subject: [PATCH 02/12] Yoni - shot stats --- .../src/fuel/calculations/fuel-averaging.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 5e4044f..2675c06 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -2,6 +2,12 @@ import type { Match, ShootEvent } from "@repo/scouting_types"; import type { BPS, FuelObject } from "../fuel-object"; import { calculateSum, firstElement, lastElement } from "@repo/array-functions"; +import { convertPixelToCentimeters, distanceFromHub } from "@repo/rebuilt_map"; + +interface ShotStats { + duration: number; + distance: number; +} const EMPTY_INTERVAL_DURATION = 0; const FIRST_INTERVAL_BOUNDARY = 0; @@ -15,10 +21,10 @@ const LAST_SECTION_LENGTH = 1; */ const calculateBallAmount = ( sections: number[][], - shotLength: number, + shotLength: ShotStats, ): number => { // Base Case 1 - if (shotLength <= EMPTY_INTERVAL_DURATION) { + if (shotLength.duration <= EMPTY_INTERVAL_DURATION) { return NO_FUEL_COLLECTED; } // Base Case 2: Happens if no section is long enough for the shot length @@ -26,7 +32,7 @@ const calculateBallAmount = ( const onlySection = firstElement(sections); const ballAmount = onlySection.length; const sectionDuration = lastElement(onlySection); - return (ballAmount / sectionDuration) * shotLength; + return (ballAmount / sectionDuration) * shotLength.duration; } // finds the average for the first interval, removes it and then recurses @@ -38,7 +44,8 @@ const calculateBallAmount = ( ); const firstIntervalSections = adjustedSections.map((section) => section.filter( - (timing) => timing <= FIRST_INTERVAL_BOUNDARY && timing <= shotLength, + (timing) => + timing <= FIRST_INTERVAL_BOUNDARY && timing <= shotLength.duration, ), ); @@ -53,7 +60,10 @@ const calculateBallAmount = ( ); return ( avgBallsFirstInterval + - calculateBallAmount(nonFirstSections, shotLength - firstIntervalDuration) + calculateBallAmount(nonFirstSections, { + duration: shotLength.duration - firstIntervalDuration, + distance: shotLength.distance, + }) ); }; @@ -67,14 +77,15 @@ const correctSectionToTimeFromEnd = (sections: number[]) => { .map((timestamp) => endTimestamp - timestamp); }; +const LAST_BALL = 1; const calculateFullSectionsBallAmount = ( sections: number[][], - shotLength: number, + shot: ShotStats, ) => calculateBallAmount( sections.map(correctSectionToTimeFromEnd).sort(compareSections), - shotLength, - ); + shot, + ) + LAST_BALL; export const calculateFuelByAveraging = ( shot: ShootEvent, @@ -82,15 +93,19 @@ export const calculateFuelByAveraging = ( sections: BPS["events"], ): FuelObject => { const shotLength = shot.interval.end - shot.interval.start; + const shotStats: ShotStats = { + duration: shotLength, + distance: distanceFromHub(convertPixelToCentimeters(shot.startPosition)), + }; const scoredAmount = calculateFullSectionsBallAmount( sections.map((section) => section.score), - shotLength, + shotStats, ); const shotAmount = calculateFullSectionsBallAmount( sections.map((section) => section.shoot), - shotLength, + shotStats, ); return { From fd41ebcfaaec7b393382325b4f4c4206680d4bde Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 17:22:12 +0200 Subject: [PATCH 03/12] Yoni - added interpolation --- .../src/fuel/calculations/interpolation.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/scouting/backend/src/fuel/calculations/interpolation.ts diff --git a/apps/scouting/backend/src/fuel/calculations/interpolation.ts b/apps/scouting/backend/src/fuel/calculations/interpolation.ts new file mode 100644 index 0000000..ac2ee84 --- /dev/null +++ b/apps/scouting/backend/src/fuel/calculations/interpolation.ts @@ -0,0 +1,27 @@ +// בס"ד + +import { firstElement } from "@repo/array-functions"; +import type { Point } from "@repo/scouting_types"; + +const QUADRATIC_EXPONENT = 2; +const interpolateTwoPointQuadratic = (x: number, p1: Point, p2: Point) => { + // Assumes p1 is the "peak" (e.g., distance 0) + const a = (p2.y - p1.y) / p2.x ** QUADRATIC_EXPONENT; + return a * x ** QUADRATIC_EXPONENT + p1.y; +}; + +const ONE_ITEM = 1; +export const interpolateQuadratic = (x: number, points: Point[]): number => { + const [first, second] = points; + + // array destructuring can make the second undefined, but ESLint doesnt know that + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (first.x >= x || second === undefined) { + return first.y; + } + if (x < second.x) { + return interpolateQuadratic(x, points.slice(ONE_ITEM)); + } + + return interpolateTwoPointQuadratic(x, first, second); +}; From 8a10a2a53599b3a13341d39143955159b3e1e444 Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 17:29:16 +0200 Subject: [PATCH 04/12] Yoni - moved bps and shi --- .../src/fuel/calculations/fuel-averaging.ts | 18 +++++++++++------- .../src/fuel/calculations/fuel-match.ts | 3 +-- .../src/fuel/calculations/interpolation.ts | 1 - apps/scouting/backend/src/fuel/fuel-object.ts | 13 +++++++------ .../backend/src/routes/teams-router.ts | 3 ++- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 2675c06..7e72235 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -1,6 +1,5 @@ // בס"ד -import type { Match, ShootEvent } from "@repo/scouting_types"; -import type { BPS, FuelObject } from "../fuel-object"; +import type { BPS, FuelObject, Match, ShootEvent } from "@repo/scouting_types"; import { calculateSum, firstElement, lastElement } from "@repo/array-functions"; import { convertPixelToCentimeters, distanceFromHub } from "@repo/rebuilt_map"; @@ -67,6 +66,14 @@ const calculateBallAmount = ( ); }; +const calculateAccuracies = (sections: BPS["events"], shotDuration: number) => { + + + const accuracies = sections.map( + (section) => section.score.length / section.shoot.length, + ); +}; + const compareSections = (a: number[], b: number[]) => lastElement(a) - lastElement(b); @@ -98,16 +105,13 @@ export const calculateFuelByAveraging = ( distance: distanceFromHub(convertPixelToCentimeters(shot.startPosition)), }; - const scoredAmount = calculateFullSectionsBallAmount( - sections.map((section) => section.score), - shotStats, - ); - const shotAmount = calculateFullSectionsBallAmount( sections.map((section) => section.shoot), shotStats, ); + const scoredAmount = 0; + return { scored: scoredAmount, shot: shotAmount, diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-match.ts b/apps/scouting/backend/src/fuel/calculations/fuel-match.ts index 7f8fead..ac5c982 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-match.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-match.ts @@ -1,6 +1,5 @@ // בס"ד -import type { ShootEvent } from "@repo/scouting_types"; -import type { BPS, FuelObject } from "../fuel-object"; +import type { BPS, FuelObject, ShootEvent } from "@repo/scouting_types"; const getIncludedShots = (section: number[], shot: ShootEvent) => { return section.filter( diff --git a/apps/scouting/backend/src/fuel/calculations/interpolation.ts b/apps/scouting/backend/src/fuel/calculations/interpolation.ts index ac2ee84..2b14a15 100644 --- a/apps/scouting/backend/src/fuel/calculations/interpolation.ts +++ b/apps/scouting/backend/src/fuel/calculations/interpolation.ts @@ -1,6 +1,5 @@ // בס"ד -import { firstElement } from "@repo/array-functions"; import type { Point } from "@repo/scouting_types"; const QUADRATIC_EXPONENT = 2; diff --git a/apps/scouting/backend/src/fuel/fuel-object.ts b/apps/scouting/backend/src/fuel/fuel-object.ts index 724a3fa..20eefa3 100644 --- a/apps/scouting/backend/src/fuel/fuel-object.ts +++ b/apps/scouting/backend/src/fuel/fuel-object.ts @@ -1,13 +1,14 @@ // בס"ד -import type { GameObject, Match, Point, ShootEvent } from "@repo/scouting_types"; +import type { + BPS, + GameObject, + Match, + Point, + ShootEvent, +} from "@repo/scouting_types"; import { calculateFuelByAveraging } from "./calculations/fuel-averaging"; import { calculateFuelByMatch } from "./calculations/fuel-match"; -export interface BPS { - events: { shoot: number[]; score: number[] }[]; - match: Match; -} - export type FuelEvents = "scored" | "shot" | "missed"; export type FuelObject = GameObject< FuelEvents, diff --git a/apps/scouting/backend/src/routes/teams-router.ts b/apps/scouting/backend/src/routes/teams-router.ts index 1558ff0..3634414 100644 --- a/apps/scouting/backend/src/routes/teams-router.ts +++ b/apps/scouting/backend/src/routes/teams-router.ts @@ -17,6 +17,7 @@ import { getFormsCollection } from "./forms-router"; import { StatusCodes } from "http-status-codes"; import { castItem } from "@repo/type-utils"; import type { + BPS, Match, ScoutingForm, SectionTeamData, @@ -26,7 +27,7 @@ import type { import { ACCURACY_DISTANCES, teamsProps } from "@repo/scouting_types"; import { groupBy } from "fp-ts/lib/NonEmptyArray"; import { calculateSum, isEmpty, mapObject } from "@repo/array-functions"; -import { createFuelObject, type BPS } from "../fuel/fuel-object"; +import { createFuelObject } from "../fuel/fuel-object"; import { splitByDistances } from "../fuel/distance-split"; import { calculateFuelStatisticsOfShift } from "../fuel/fuel-general"; From 1dc0c43b9d4c8cad370d02fdb195d82a0ab2456c Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 17:39:32 +0200 Subject: [PATCH 05/12] Yoni - finished prototype --- .../src/fuel/calculations/fuel-averaging.ts | 39 ++++++++++++++----- .../backend/src/routes/teams-router.ts | 1 + .../scouting_types/rebuilt/fuel/FuelTypes.ts | 2 +- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 7e72235..fff750d 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -1,7 +1,13 @@ // בס"ד import type { BPS, FuelObject, Match, ShootEvent } from "@repo/scouting_types"; -import { calculateSum, firstElement, lastElement } from "@repo/array-functions"; +import { + calculateAverage, + calculateSum, + firstElement, + lastElement, +} from "@repo/array-functions"; import { convertPixelToCentimeters, distanceFromHub } from "@repo/rebuilt_map"; +import { interpolateQuadratic } from "./interpolation"; interface ShotStats { duration: number; @@ -67,11 +73,20 @@ const calculateBallAmount = ( }; const calculateAccuracies = (sections: BPS["events"], shotDuration: number) => { - - - const accuracies = sections.map( - (section) => section.score.length / section.shoot.length, - ); + const durationedSections = sections.map((section) => ({ + shoot: section.shoot.filter((timestamp) => timestamp <= shotDuration), + score: section.score.filter((timestamp) => timestamp <= shotDuration), + positions: section.positions, + })); + + const accuracies = durationedSections.map((section) => ({ + distance: calculateAverage(section.positions, (point) => + distanceFromHub(convertPixelToCentimeters(point)), + ), + accuracy: section.score.length / section.shoot.length, + })); + const sortedAccuracies = accuracies.sort((a, b) => a.distance - b.distance); + return sortedAccuracies; }; const compareSections = (a: number[], b: number[]) => @@ -99,9 +114,8 @@ export const calculateFuelByAveraging = ( match: Match, sections: BPS["events"], ): FuelObject => { - const shotLength = shot.interval.end - shot.interval.start; const shotStats: ShotStats = { - duration: shotLength, + duration: shot.interval.end - shot.interval.start, distance: distanceFromHub(convertPixelToCentimeters(shot.startPosition)), }; @@ -110,7 +124,14 @@ export const calculateFuelByAveraging = ( shotStats, ); - const scoredAmount = 0; + const scoredAccuracy = interpolateQuadratic( + shotStats.distance, + calculateAccuracies(sections, shotStats.duration).map( + ({ distance, accuracy }) => ({ x: distance, y: accuracy }), + ), + ); + + const scoredAmount = shotAmount * scoredAccuracy; return { scored: scoredAmount, diff --git a/apps/scouting/backend/src/routes/teams-router.ts b/apps/scouting/backend/src/routes/teams-router.ts index 3634414..5d7d4a9 100644 --- a/apps/scouting/backend/src/routes/teams-router.ts +++ b/apps/scouting/backend/src/routes/teams-router.ts @@ -125,6 +125,7 @@ const getBPSes = (): BPS[] => [ score: [1000, 2000, 3000], // eslint-disable-next-line @typescript-eslint/no-magic-numbers shoot: [1000, 1400, 2000, 3000], + positions: [{ x: 300, y: 200 }], }, ], match: { type: "qualification", number: 8 }, diff --git a/packages/scouting_types/rebuilt/fuel/FuelTypes.ts b/packages/scouting_types/rebuilt/fuel/FuelTypes.ts index 13debb0..76a7871 100644 --- a/packages/scouting_types/rebuilt/fuel/FuelTypes.ts +++ b/packages/scouting_types/rebuilt/fuel/FuelTypes.ts @@ -12,7 +12,7 @@ export type GameTime = keyof GeneralFuelData; export interface BPS { - events: { shoot: number[]; score: number[] }[]; + events: { shoot: number[]; score: number[] ,positions: Point[]}[]; match: Match; } From 236b1bd9cdf030c12ebdf97473a59482720f307d Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 17:43:34 +0200 Subject: [PATCH 06/12] Yoni - qual 10 --- apps/scouting/backend/src/routes/teams-router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scouting/backend/src/routes/teams-router.ts b/apps/scouting/backend/src/routes/teams-router.ts index 5d7d4a9..9539582 100644 --- a/apps/scouting/backend/src/routes/teams-router.ts +++ b/apps/scouting/backend/src/routes/teams-router.ts @@ -128,7 +128,7 @@ const getBPSes = (): BPS[] => [ positions: [{ x: 300, y: 200 }], }, ], - match: { type: "qualification", number: 8 }, + match: { type: "qualification", number: 10 }, }, ]; From 9e4817aa95739df67b0975f278909d8354fde4cc Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 20:43:37 +0200 Subject: [PATCH 07/12] Yoni - added isEmpty --- .../src/fuel/calculations/fuel-averaging.ts | 37 ++++++++++++------- .../src/fuel/calculations/fuel-match.ts | 1 + .../src/fuel/calculations/interpolation.ts | 7 +++- apps/scouting/backend/src/fuel/fuel-object.ts | 1 - 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 5e0b3a1..52aa871 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -4,6 +4,7 @@ import { calculateAverage, calculateSum, firstElement, + isEmpty, lastElement, } from "@repo/array-functions"; import { convertPixelToCentimeters, distanceFromHub } from "@repo/rebuilt_map"; @@ -79,13 +80,18 @@ const calculateAccuracies = (sections: BPS["events"], shotDuration: number) => { positions: section.positions, })); - const accuracies = durationedSections.map((section) => ({ + const filteredSections = durationedSections.filter( + (section) => !isEmpty(section.shoot), + ); + + const accuracies = filteredSections.map((section) => ({ distance: calculateAverage(section.positions, (point) => distanceFromHub(convertPixelToCentimeters(point)), ), accuracy: section.score.length / section.shoot.length, })); const sortedAccuracies = accuracies.sort((a, b) => a.distance - b.distance); + return sortedAccuracies; }; @@ -99,15 +105,16 @@ const correctSectionToTimeFromEnd = (sections: number[]) => { .map((timestamp) => endTimestamp - timestamp); }; +const formatSections = (sections: BPS["events"]) => + sections + .map((section) => ({ + ...section, + score: correctSectionToTimeFromEnd(section.score), + shoot: correctSectionToTimeFromEnd(section.shoot), + })) + .sort((a, b) => compareSections(a.shoot, b.shoot)); + const LAST_BALL = 1; -const calculateFullSectionsBallAmount = ( - sections: number[][], - shot: ShotStats, -) => - calculateBallAmount( - sections.map(correctSectionToTimeFromEnd).sort(compareSections), - shot, - ) + LAST_BALL; export const calculateFuelByAveraging = ( shot: ShootEvent, @@ -118,10 +125,12 @@ export const calculateFuelByAveraging = ( duration: shot.interval.end - shot.interval.start, distance: distanceFromHub(convertPixelToCentimeters(shot.startPosition)), }; - const shotAmount = calculateFullSectionsBallAmount( - sections.map((section) => section.shoot), - shotStats, - ); + + const shotAmount = + calculateBallAmount( + formatSections(sections).map((section) => section.shoot), + shotStats, + ) + LAST_BALL; if (isPass) { return { @@ -132,7 +141,7 @@ export const calculateFuelByAveraging = ( } const scoredAccuracy = interpolateQuadratic( shotStats.distance, - calculateAccuracies(sections, shotStats.duration).map( + calculateAccuracies(formatSections(sections), shotStats.duration).map( ({ distance, accuracy }) => ({ x: distance, y: accuracy }), ), ); diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-match.ts b/apps/scouting/backend/src/fuel/calculations/fuel-match.ts index 896e016..155d606 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-match.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-match.ts @@ -29,6 +29,7 @@ export const calculateFuelByMatch = ( const scoredAmount = shotBps.flatMap((section) => section.score).length; + return { shot: shotAmount, scored: scoredAmount, diff --git a/apps/scouting/backend/src/fuel/calculations/interpolation.ts b/apps/scouting/backend/src/fuel/calculations/interpolation.ts index 2b14a15..5ca106c 100644 --- a/apps/scouting/backend/src/fuel/calculations/interpolation.ts +++ b/apps/scouting/backend/src/fuel/calculations/interpolation.ts @@ -1,5 +1,6 @@ // בס"ד +import { isEmpty } from "@repo/array-functions"; import type { Point } from "@repo/scouting_types"; const QUADRATIC_EXPONENT = 2; @@ -10,7 +11,11 @@ const interpolateTwoPointQuadratic = (x: number, p1: Point, p2: Point) => { }; const ONE_ITEM = 1; -export const interpolateQuadratic = (x: number, points: Point[]): number => { +const DEFAULT_EMPTY_VALUE = 0; +export const interpolateQuadratic = (x: number, points: Point[], emptyValue = DEFAULT_EMPTY_VALUE): number => { + if (isEmpty(points)) { + return emptyValue; + } const [first, second] = points; // array destructuring can make the second undefined, but ESLint doesnt know that diff --git a/apps/scouting/backend/src/fuel/fuel-object.ts b/apps/scouting/backend/src/fuel/fuel-object.ts index 0f312e3..323e7ac 100644 --- a/apps/scouting/backend/src/fuel/fuel-object.ts +++ b/apps/scouting/backend/src/fuel/fuel-object.ts @@ -10,7 +10,6 @@ import { calculateFuelByAveraging } from "./calculations/fuel-averaging"; import { calculateFuelByMatch } from "./calculations/fuel-match"; import { ALLIANCE_ZONE_WIDTH_PIXELS } from "@repo/rebuilt_map"; - const isShotPass = (positionPixels: Point) => positionPixels.x > ALLIANCE_ZONE_WIDTH_PIXELS; From da0665a40a3957cbc4363cb83735eb7d8e6030ee Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 11 Feb 2026 20:58:38 +0200 Subject: [PATCH 08/12] Yoni - changed name to shot --- .../backend/src/fuel/calculations/fuel-averaging.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 52aa871..2ff345a 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -27,10 +27,10 @@ const LAST_SECTION_LENGTH = 1; */ const calculateBallAmount = ( sections: number[][], - shotLength: ShotStats, + shot: ShotStats, ): number => { // Base Case 1 - if (shotLength.duration <= EMPTY_INTERVAL_DURATION) { + if (shot.duration <= EMPTY_INTERVAL_DURATION) { return NO_FUEL_COLLECTED; } // Base Case 2: Happens if no section is long enough for the shot length @@ -38,7 +38,7 @@ const calculateBallAmount = ( const onlySection = firstElement(sections); const ballAmount = onlySection.length; const sectionDuration = lastElement(onlySection); - return (ballAmount / sectionDuration) * shotLength.duration; + return (ballAmount / sectionDuration) * shot.duration; } // finds the average for the first interval, removes it and then recurses @@ -51,7 +51,7 @@ const calculateBallAmount = ( const firstIntervalSections = adjustedSections.map((section) => section.filter( (timing) => - timing <= FIRST_INTERVAL_BOUNDARY && timing <= shotLength.duration, + timing <= FIRST_INTERVAL_BOUNDARY && timing <= shot.duration, ), ); @@ -67,8 +67,8 @@ const calculateBallAmount = ( return ( avgBallsFirstInterval + calculateBallAmount(nonFirstSections, { - duration: shotLength.duration - firstIntervalDuration, - distance: shotLength.distance, + duration: shot.duration - firstIntervalDuration, + distance: shot.distance, }) ); }; From 92bf1b3589acb7a358d8a373964727f91d165a36 Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Thu, 12 Feb 2026 18:47:16 +0200 Subject: [PATCH 09/12] Yoni - fixed according to CR --- .../src/fuel/calculations/fuel-averaging.ts | 18 ++++++++---------- .../src/fuel/calculations/interpolation.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 2ff345a..a82d97a 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -25,10 +25,7 @@ const LAST_SECTION_LENGTH = 1; * @param sections consists of sections that contains a list of timestamps in ms * @returns mean ball amount */ -const calculateBallAmount = ( - sections: number[][], - shot: ShotStats, -): number => { +const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { // Base Case 1 if (shot.duration <= EMPTY_INTERVAL_DURATION) { return NO_FUEL_COLLECTED; @@ -50,8 +47,7 @@ const calculateBallAmount = ( ); const firstIntervalSections = adjustedSections.map((section) => section.filter( - (timing) => - timing <= FIRST_INTERVAL_BOUNDARY && timing <= shot.duration, + (timing) => timing <= FIRST_INTERVAL_BOUNDARY && timing <= shot.duration, ), ); @@ -90,13 +86,13 @@ const calculateAccuracies = (sections: BPS["events"], shotDuration: number) => { ), accuracy: section.score.length / section.shoot.length, })); - const sortedAccuracies = accuracies.sort((a, b) => a.distance - b.distance); + const sortedAccuracies = accuracies.sort((accuracy1, accuracy2) => accuracy1.distance - accuracy2.distance); return sortedAccuracies; }; -const compareSections = (a: number[], b: number[]) => - lastElement(a) - lastElement(b); +const compareSections = (section1: number[], section2: number[]) => + lastElement(section1) - lastElement(section2); const correctSectionToTimeFromEnd = (sections: number[]) => { const endTimestamp = lastElement(sections); @@ -112,7 +108,9 @@ const formatSections = (sections: BPS["events"]) => score: correctSectionToTimeFromEnd(section.score), shoot: correctSectionToTimeFromEnd(section.shoot), })) - .sort((a, b) => compareSections(a.shoot, b.shoot)); + .sort((formattedSection1, formattedSection2) => + compareSections(formattedSection1.shoot, formattedSection2.shoot), + ); const LAST_BALL = 1; diff --git a/apps/scouting/backend/src/fuel/calculations/interpolation.ts b/apps/scouting/backend/src/fuel/calculations/interpolation.ts index 5ca106c..cce84a5 100644 --- a/apps/scouting/backend/src/fuel/calculations/interpolation.ts +++ b/apps/scouting/backend/src/fuel/calculations/interpolation.ts @@ -12,15 +12,19 @@ const interpolateTwoPointQuadratic = (x: number, p1: Point, p2: Point) => { const ONE_ITEM = 1; const DEFAULT_EMPTY_VALUE = 0; -export const interpolateQuadratic = (x: number, points: Point[], emptyValue = DEFAULT_EMPTY_VALUE): number => { +const LENGTH_THAT_DOESNT_INCLUDE_TWO_ITEMS = 2; +export const interpolateQuadratic = ( + x: number, + points: Point[], + emptyValue = DEFAULT_EMPTY_VALUE, +): number => { if (isEmpty(points)) { return emptyValue; } + const [first, second] = points; - // array destructuring can make the second undefined, but ESLint doesnt know that - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (first.x >= x || second === undefined) { + if (first.x >= x || points.length === LENGTH_THAT_DOESNT_INCLUDE_TWO_ITEMS) { return first.y; } if (x < second.x) { From 5e70e3becf8b4ef44f4cde7f0a17b79add11754e Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Thu, 12 Feb 2026 18:53:37 +0200 Subject: [PATCH 10/12] Yoni - corrected length --- apps/scouting/backend/src/fuel/calculations/interpolation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scouting/backend/src/fuel/calculations/interpolation.ts b/apps/scouting/backend/src/fuel/calculations/interpolation.ts index cce84a5..86dfb5f 100644 --- a/apps/scouting/backend/src/fuel/calculations/interpolation.ts +++ b/apps/scouting/backend/src/fuel/calculations/interpolation.ts @@ -12,7 +12,7 @@ const interpolateTwoPointQuadratic = (x: number, p1: Point, p2: Point) => { const ONE_ITEM = 1; const DEFAULT_EMPTY_VALUE = 0; -const LENGTH_THAT_DOESNT_INCLUDE_TWO_ITEMS = 2; +const LENGTH_THAT_DOESNT_INCLUDE_TWO_ITEMS = 1; export const interpolateQuadratic = ( x: number, points: Point[], From 778fda76256b40896461c3ef62e1712a7cbf6813 Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 18 Feb 2026 17:00:31 +0200 Subject: [PATCH 11/12] Yoni - reversed list going in average to make it in the right order --- .../src/fuel/calculations/fuel-averaging.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index a82d97a..4b3ae02 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -30,6 +30,7 @@ const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { if (shot.duration <= EMPTY_INTERVAL_DURATION) { return NO_FUEL_COLLECTED; } + // Base Case 2: Happens if no section is long enough for the shot length if (sections.length === LAST_SECTION_LENGTH) { const onlySection = firstElement(sections); @@ -45,6 +46,7 @@ const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { const adjustedSections = sections.map((section) => section.map((timing) => timing - firstIntervalDuration), ); + const firstIntervalSections = adjustedSections.map((section) => section.filter( (timing) => timing <= FIRST_INTERVAL_BOUNDARY && timing <= shot.duration, @@ -60,6 +62,7 @@ const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { .map((section) => section.filter((timing) => timing > FIRST_INTERVAL_BOUNDARY), ); + return ( avgBallsFirstInterval + calculateBallAmount(nonFirstSections, { @@ -86,7 +89,9 @@ const calculateAccuracies = (sections: BPS["events"], shotDuration: number) => { ), accuracy: section.score.length / section.shoot.length, })); - const sortedAccuracies = accuracies.sort((accuracy1, accuracy2) => accuracy1.distance - accuracy2.distance); + const sortedAccuracies = accuracies.sort( + (accuracy1, accuracy2) => accuracy1.distance - accuracy2.distance, + ); return sortedAccuracies; }; @@ -96,9 +101,7 @@ const compareSections = (section1: number[], section2: number[]) => const correctSectionToTimeFromEnd = (sections: number[]) => { const endTimestamp = lastElement(sections); - return sections - .filter((timestamp) => timestamp < endTimestamp) - .map((timestamp) => endTimestamp - timestamp); + return sections.map((timestamp) => endTimestamp - timestamp).reverse(); }; const formatSections = (sections: BPS["events"]) => @@ -112,8 +115,6 @@ const formatSections = (sections: BPS["events"]) => compareSections(formattedSection1.shoot, formattedSection2.shoot), ); -const LAST_BALL = 1; - export const calculateFuelByAveraging = ( shot: ShootEvent, isPass: boolean, @@ -124,11 +125,12 @@ export const calculateFuelByAveraging = ( distance: distanceFromHub(convertPixelToCentimeters(shot.startPosition)), }; - const shotAmount = - calculateBallAmount( - formatSections(sections).map((section) => section.shoot), - shotStats, - ) + LAST_BALL; + const formattedSections = formatSections(sections); + + const shotAmount = calculateBallAmount( + formattedSections.map((section) => section.shoot), + shotStats, + ); if (isPass) { return { @@ -139,7 +141,7 @@ export const calculateFuelByAveraging = ( } const scoredAccuracy = interpolateQuadratic( shotStats.distance, - calculateAccuracies(formatSections(sections), shotStats.duration).map( + calculateAccuracies(formattedSections, shotStats.duration).map( ({ distance, accuracy }) => ({ x: distance, y: accuracy }), ), ); From 6e049c2ea42f573f01703ec0c41b1de3625d1575 Mon Sep 17 00:00:00 2001 From: YoniKiriaty Date: Wed, 18 Feb 2026 20:22:14 +0200 Subject: [PATCH 12/12] Yoni - fixed comments --- .../src/fuel/calculations/fuel-averaging.ts | 35 ++++++++++--------- .../scouting_types/rebuilt/fuel/FuelTypes.ts | 8 ++++- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts index 4b3ae02..7d9ca80 100644 --- a/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts +++ b/apps/scouting/backend/src/fuel/calculations/fuel-averaging.ts @@ -1,5 +1,5 @@ // בס"ד -import type { BPS, FuelObject, ShootEvent } from "@repo/scouting_types"; +import type { BPS, BPSEvent, FuelObject, ShootEvent } from "@repo/scouting_types"; import { calculateAverage, calculateSum, @@ -11,15 +11,15 @@ import { convertPixelToCentimeters, distanceFromHub } from "@repo/rebuilt_map"; import { interpolateQuadratic } from "./interpolation"; interface ShotStats { - duration: number; - distance: number; + durationMilliseconds: number; + hubDistanceCentimeters: number; } const EMPTY_INTERVAL_DURATION = 0; const FIRST_INTERVAL_BOUNDARY = 0; const NO_FUEL_COLLECTED = 0; const FIRST_SECTION_AMOUNT = 1; -const LAST_SECTION_LENGTH = 1; +const ONE_SECTION_ONLY_LENGTH = 1; /** * @param sections consists of sections that contains a list of timestamps in ms @@ -27,16 +27,15 @@ const LAST_SECTION_LENGTH = 1; */ const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { // Base Case 1 - if (shot.duration <= EMPTY_INTERVAL_DURATION) { + if (shot.durationMilliseconds <= EMPTY_INTERVAL_DURATION) { return NO_FUEL_COLLECTED; } - // Base Case 2: Happens if no section is long enough for the shot length - if (sections.length === LAST_SECTION_LENGTH) { + if (sections.length === ONE_SECTION_ONLY_LENGTH) { const onlySection = firstElement(sections); const ballAmount = onlySection.length; const sectionDuration = lastElement(onlySection); - return (ballAmount / sectionDuration) * shot.duration; + return (ballAmount / sectionDuration) * shot.durationMilliseconds; } // finds the average for the first interval, removes it and then recurses @@ -49,7 +48,9 @@ const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { const firstIntervalSections = adjustedSections.map((section) => section.filter( - (timing) => timing <= FIRST_INTERVAL_BOUNDARY && timing <= shot.duration, + (timing) => + timing <= FIRST_INTERVAL_BOUNDARY && + timing <= shot.durationMilliseconds, ), ); @@ -66,8 +67,8 @@ const calculateBallAmount = (sections: number[][], shot: ShotStats): number => { return ( avgBallsFirstInterval + calculateBallAmount(nonFirstSections, { - duration: shot.duration - firstIntervalDuration, - distance: shot.distance, + durationMilliseconds: shot.durationMilliseconds - firstIntervalDuration, + hubDistanceCentimeters: shot.hubDistanceCentimeters, }) ); }; @@ -118,11 +119,13 @@ const formatSections = (sections: BPS["events"]) => export const calculateFuelByAveraging = ( shot: ShootEvent, isPass: boolean, - sections: BPS["events"], + sections: BPSEvent[], ): Partial => { const shotStats: ShotStats = { - duration: shot.interval.end - shot.interval.start, - distance: distanceFromHub(convertPixelToCentimeters(shot.startPosition)), + durationMilliseconds: shot.interval.end - shot.interval.start, + hubDistanceCentimeters: distanceFromHub( + convertPixelToCentimeters(shot.startPosition), + ), }; const formattedSections = formatSections(sections); @@ -140,8 +143,8 @@ export const calculateFuelByAveraging = ( }; } const scoredAccuracy = interpolateQuadratic( - shotStats.distance, - calculateAccuracies(formattedSections, shotStats.duration).map( + shotStats.hubDistanceCentimeters, + calculateAccuracies(formattedSections, shotStats.durationMilliseconds).map( ({ distance, accuracy }) => ({ x: distance, y: accuracy }), ), ); diff --git a/packages/scouting_types/rebuilt/fuel/FuelTypes.ts b/packages/scouting_types/rebuilt/fuel/FuelTypes.ts index 55e0b52..3c26be6 100644 --- a/packages/scouting_types/rebuilt/fuel/FuelTypes.ts +++ b/packages/scouting_types/rebuilt/fuel/FuelTypes.ts @@ -10,8 +10,14 @@ export interface GeneralFuelData { export type GameTime = keyof GeneralFuelData; +export interface BPSEvent { + shoot: number[]; + score: number[]; + positions: Point[]; +} + export interface BPS { - events: { shoot: number[]; score: number[] ,positions: Point[]}[]; + events: BPSEvent[]; match: Match; }