import * as math from 'mathjs'; import { Phase, type Stat, type Surface } from './race'; export interface Race { location: RaceLocation; length: number; surface: Surface; direction: Direction; spans: Span[]; slopes: Slope[]; thresholds: [] | [Stat] | [Stat, Stat]; } export enum RaceLocation { Sapporo = 'Sapporo', Hakodate = 'Hakodate', Niigata = 'Niigata', Fukushima = 'Fukushima', Nakayama = 'Nakayama', Tokyo = 'Tokyo', Chukyo = 'Chukyo', Kyoto = 'Kyoto', Hanshin = 'Hanshin', Kokura = 'Kokura', Ooi = 'Ooi', Kawasaki = 'Kawasaki', Funabashi = 'Funabashi', Morioka = 'Morioka', Longchamp = 'Longchamp', SantaAnitaPark = 'Santa Anita Park', DelMar = 'Del Mar', } export enum Direction { Right, Left, Stretch, } export enum SpanType { FrontStraight, AcrossStraight, FalseStraight, Corner1, Corner2, Corner3, Corner4, } export interface Span { type: SpanType; span: [number, number]; } export interface Slope { per: -2 | -1.5 | -1 | 1 | 1.5 | 2; span: [number, number]; } function sortSlopes(slopes: Slope[]): Slope[] { return slopes.toSorted((a, b) => a.span[0] - b.span[0]); } /** * Get the distances into a race where each phase ends. * @param raceLen Length of the race in meters * @returns Quadruple of phase end points, including separate late and last spurt phases */ export function phases(raceLen: number): [number, number, number, number] { const sixth = raceLen / 6; const half = raceLen / 2; return [sixth, sixth + half, 2 * sixth + half, raceLen]; } export interface SectionInfo { section: number; phase: Phase; endRate: number; posKeep: boolean; rush: boolean; spotStruggleEnter: boolean; } export const SECTIONS: ReadonlyArray = [ { section: 1, phase: Phase.EarlyRace, endRate: 1 / 24, posKeep: true, rush: false, spotStruggleEnter: false }, { section: 2, phase: Phase.EarlyRace, endRate: 2 / 24, posKeep: true, rush: true, spotStruggleEnter: true }, { section: 3, phase: Phase.EarlyRace, endRate: 3 / 24, posKeep: true, rush: true, spotStruggleEnter: true }, { section: 4, phase: Phase.EarlyRace, endRate: 4 / 24, posKeep: true, rush: true, spotStruggleEnter: true }, { section: 5, phase: Phase.MidRace, endRate: 5 / 24, posKeep: true, rush: true, spotStruggleEnter: true }, { section: 6, phase: Phase.MidRace, endRate: 6 / 24, posKeep: true, rush: true, spotStruggleEnter: false }, { section: 7, phase: Phase.MidRace, endRate: 7 / 24, posKeep: true, rush: true, spotStruggleEnter: false }, { section: 8, phase: Phase.MidRace, endRate: 8 / 24, posKeep: true, rush: true, spotStruggleEnter: false }, { section: 9, phase: Phase.MidRace, endRate: 9 / 24, posKeep: true, rush: true, spotStruggleEnter: false }, { section: 10, phase: Phase.MidRace, endRate: 10 / 24, posKeep: true, rush: false, spotStruggleEnter: false }, { section: 11, phase: Phase.MidRace, endRate: 11 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 12, phase: Phase.MidRace, endRate: 12 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 13, phase: Phase.MidRace, endRate: 13 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 14, phase: Phase.MidRace, endRate: 14 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 15, phase: Phase.MidRace, endRate: 15 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 16, phase: Phase.MidRace, endRate: 16 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 17, phase: Phase.LateRace, endRate: 17 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 18, phase: Phase.LateRace, endRate: 18 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 19, phase: Phase.LateRace, endRate: 19 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 20, phase: Phase.LateRace, endRate: 20 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 21, phase: Phase.LateRace, endRate: 21 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 22, phase: Phase.LateRace, endRate: 22 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 23, phase: Phase.LateRace, endRate: 23 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, { section: 24, phase: Phase.LateRace, endRate: 24 / 24, posKeep: false, rush: false, spotStruggleEnter: false }, ]; export interface SectionSlopeSegment extends SectionInfo { startDist: number; endDist: number; slope: -2 | -1.5 | -1 | 0 | 1 | 1.5 | 2; } export function splitSectionSlopes(raceLen: number, slopes: Slope[]): SectionSlopeSegment[] { const ss = sortSlopes(slopes); return SECTIONS.flatMap((sec) => { const startDist = ((sec.section - 1) * raceLen) / 24; const endDist = (sec.section * raceLen) / 24; const s = ss.filter((sl) => sl.span[0] < endDist && sl.span[1] > startDist); if (s.length === 0) { return [{ ...sec, startDist, endDist, slope: 0 }]; } const r: Array = [{ ...sec, startDist, endDist: math.min(s[0].span[0], endDist), slope: 0 }]; for (const [i, sl] of s.entries()) { const slopeEnd = math.min(endDist, sl.span[1]); r.push( { ...sec, startDist: math.max(startDist, sl.span[0]), endDist: slopeEnd, slope: sl.per }, { ...sec, startDist: slopeEnd, endDist: math.min(s[i + 1]?.span[0] ?? endDist, endDist), slope: 0 }, ); } return r.filter((sl) => sl.startDist < sl.endDist); }); }