mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Upgrade live timing page for race weekends
- Track status flag banner (green/yellow/SC/VSC/red, defensive mapping) - Weather strip: air/track temp, humidity, wind with compass, rain badge - Gap trend sparklines from a per-driver interval ring buffer - Battle detection: consecutive cars within 1.0s chained and highlighted - Stint history column with compound sequence per driver - Pinnable drivers (max 3) with focus cards, persisted to localStorage Pure logic lives in lib/gapHistory.ts and lib/battles.ts with unit tests; 137 frontend tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
107
frontend/src/lib/battles.ts
Normal file
107
frontend/src/lib/battles.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// Battle detection for the live timing tower.
|
||||
// Pure functions only — no React, no side effects — so everything is unit-testable.
|
||||
|
||||
import type { LiveTimingRow } from './live'
|
||||
import { driverCode } from './live'
|
||||
import { parseIntervalSeconds } from './gapHistory'
|
||||
|
||||
export const BATTLE_THRESHOLD_SECONDS = 1.0
|
||||
|
||||
export interface BattleDriver {
|
||||
racingNumber: string
|
||||
code: string
|
||||
position: number
|
||||
/** Interval to the car ahead within the group; null for the group leader. */
|
||||
gapToAhead: number | null
|
||||
}
|
||||
|
||||
export interface Battle {
|
||||
drivers: BattleDriver[]
|
||||
/** Tightest car-to-car interval within the group. */
|
||||
minGap: number
|
||||
}
|
||||
|
||||
/** Battles are only meaningful in race-type sessions (GP race, sprint). */
|
||||
export function isRaceSession(sessionType: string | null | undefined): boolean {
|
||||
if (!sessionType) return false
|
||||
const type = sessionType.toLowerCase()
|
||||
return type.includes('race') || type.includes('sprint')
|
||||
}
|
||||
|
||||
function isEligible(row: LiveTimingRow): boolean {
|
||||
const driver = row.Driver
|
||||
return Boolean(driver) && !driver.InPit && !driver.Retired && row.Position > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan position-sorted tower rows and group consecutive cars racing within
|
||||
* `threshold` seconds of the car ahead. Cars in the pits or retired break
|
||||
* the chain, as do lapped/unparsable intervals.
|
||||
*/
|
||||
export function detectBattles(
|
||||
rows: ReadonlyArray<LiveTimingRow>,
|
||||
sessionType: string | null | undefined,
|
||||
threshold = BATTLE_THRESHOLD_SECONDS,
|
||||
): Battle[] {
|
||||
if (!isRaceSession(sessionType) || rows.length < 2) return []
|
||||
|
||||
const battles: Battle[] = []
|
||||
let current: BattleDriver[] | null = null
|
||||
let minGap = Number.POSITIVE_INFINITY
|
||||
|
||||
const flush = () => {
|
||||
if (current && current.length >= 2) {
|
||||
battles.push({ drivers: current, minGap })
|
||||
}
|
||||
current = null
|
||||
minGap = Number.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const ahead = rows[i - 1]
|
||||
const row = rows[i]
|
||||
const gap = parseIntervalSeconds(row.Driver?.Interval)
|
||||
|
||||
const inBattle =
|
||||
gap !== null && gap >= 0 && gap <= threshold && isEligible(ahead) && isEligible(row)
|
||||
|
||||
if (!inBattle) {
|
||||
flush()
|
||||
continue
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
current = [toBattleDriver(ahead, null)]
|
||||
}
|
||||
current.push(toBattleDriver(row, gap))
|
||||
if (gap < minGap) minGap = gap
|
||||
}
|
||||
flush()
|
||||
|
||||
return battles
|
||||
}
|
||||
|
||||
function toBattleDriver(row: LiveTimingRow, gapToAhead: number | null): BattleDriver {
|
||||
return {
|
||||
racingNumber: row.RacingNumber,
|
||||
code: driverCode(row),
|
||||
position: row.Position,
|
||||
gapToAhead,
|
||||
}
|
||||
}
|
||||
|
||||
/** Chip label, e.g. "VER ⚔ NOR +0.4" or "VER ⚔ NOR ⚔ PIA +0.3". */
|
||||
export function battleLabel(battle: Battle): string {
|
||||
const codes = battle.drivers.map((driver) => driver.code).join(' ⚔ ')
|
||||
const gap = Number.isFinite(battle.minGap) ? ` +${battle.minGap.toFixed(1)}` : ''
|
||||
return `${codes}${gap}`
|
||||
}
|
||||
|
||||
/** Racing numbers involved in any battle, for tower row highlighting. */
|
||||
export function battleNumbers(battles: ReadonlyArray<Battle>): Set<string> {
|
||||
const numbers = new Set<string>()
|
||||
for (const battle of battles) {
|
||||
for (const driver of battle.drivers) numbers.add(driver.racingNumber)
|
||||
}
|
||||
return numbers
|
||||
}
|
||||
Reference in New Issue
Block a user