zenno/race/stam: beginning of stamina calculator

This commit is contained in:
2026-06-17 19:55:12 -04:00
parent 7085fea1b7
commit 84f4f8e358
3 changed files with 417 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
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<SectionInfo> = [
{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<SectionSlopeSegment>((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<SectionSlopeSegment> = [{...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);
})
}