diff --git a/zenno/src/lib/course.spec.ts b/zenno/src/lib/course.spec.ts new file mode 100644 index 0000000..aea22e2 --- /dev/null +++ b/zenno/src/lib/course.spec.ts @@ -0,0 +1,195 @@ +import { expect, test } from "vitest"; +import * as course from './course'; +import { Phase } from "./race"; + +test.for<{raceLen: number, slopes: course.Slope[], want: course.SectionSlopeSegment[]}>([ + { + raceLen: 1200, + slopes: [], + want: [ + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 0, endDist: 50, slope: 0}, + {section: 2, phase: Phase.EarlyRace, endRate: 2/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 50, endDist: 100, slope: 0}, + {section: 3, phase: Phase.EarlyRace, endRate: 3/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 100, endDist: 150, slope: 0}, + {section: 4, phase: Phase.EarlyRace, endRate: 4/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 150, endDist: 200, slope: 0}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 200, endDist: 250, slope: 0}, + {section: 6, phase: Phase.MidRace, endRate: 6/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 250, endDist: 300, slope: 0}, + {section: 7, phase: Phase.MidRace, endRate: 7/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 300, endDist: 350, slope: 0}, + {section: 8, phase: Phase.MidRace, endRate: 8/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 350, endDist: 400, slope: 0}, + {section: 9, phase: Phase.MidRace, endRate: 9/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 400, endDist: 450, slope: 0}, + {section: 10, phase: Phase.MidRace, endRate: 10/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 450, endDist: 500, slope: 0}, + {section: 11, phase: Phase.MidRace, endRate: 11/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 500, endDist: 550, slope: 0}, + {section: 12, phase: Phase.MidRace, endRate: 12/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 550, endDist: 600, slope: 0}, + {section: 13, phase: Phase.MidRace, endRate: 13/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 600, endDist: 650, slope: 0}, + {section: 14, phase: Phase.MidRace, endRate: 14/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 650, endDist: 700, slope: 0}, + {section: 15, phase: Phase.MidRace, endRate: 15/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 700, endDist: 750, slope: 0}, + {section: 16, phase: Phase.MidRace, endRate: 16/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 750, endDist: 800, slope: 0}, + {section: 17, phase: Phase.LateRace, endRate: 17/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 800, endDist: 850, slope: 0}, + {section: 18, phase: Phase.LateRace, endRate: 18/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 850, endDist: 900, slope: 0}, + {section: 19, phase: Phase.LateRace, endRate: 19/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 900, endDist: 950, slope: 0}, + {section: 20, phase: Phase.LateRace, endRate: 20/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 950, endDist: 1000, slope: 0}, + {section: 21, phase: Phase.LateRace, endRate: 21/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1000, endDist: 1050, slope: 0}, + {section: 22, phase: Phase.LateRace, endRate: 22/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1050, endDist: 1100, slope: 0}, + {section: 23, phase: Phase.LateRace, endRate: 23/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1100, endDist: 1150, slope: 0}, + {section: 24, phase: Phase.LateRace, endRate: 24/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1150, endDist: 1200, slope: 0}, + ], + }, + { + raceLen: 1200, + slopes: [{per: 1, span: [0, 25]}], + want: [ + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 0, endDist: 25, slope: 1}, + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 25, endDist: 50, slope: 0}, + {section: 2, phase: Phase.EarlyRace, endRate: 2/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 50, endDist: 100, slope: 0}, + {section: 3, phase: Phase.EarlyRace, endRate: 3/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 100, endDist: 150, slope: 0}, + {section: 4, phase: Phase.EarlyRace, endRate: 4/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 150, endDist: 200, slope: 0}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 200, endDist: 250, slope: 0}, + {section: 6, phase: Phase.MidRace, endRate: 6/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 250, endDist: 300, slope: 0}, + {section: 7, phase: Phase.MidRace, endRate: 7/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 300, endDist: 350, slope: 0}, + {section: 8, phase: Phase.MidRace, endRate: 8/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 350, endDist: 400, slope: 0}, + {section: 9, phase: Phase.MidRace, endRate: 9/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 400, endDist: 450, slope: 0}, + {section: 10, phase: Phase.MidRace, endRate: 10/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 450, endDist: 500, slope: 0}, + {section: 11, phase: Phase.MidRace, endRate: 11/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 500, endDist: 550, slope: 0}, + {section: 12, phase: Phase.MidRace, endRate: 12/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 550, endDist: 600, slope: 0}, + {section: 13, phase: Phase.MidRace, endRate: 13/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 600, endDist: 650, slope: 0}, + {section: 14, phase: Phase.MidRace, endRate: 14/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 650, endDist: 700, slope: 0}, + {section: 15, phase: Phase.MidRace, endRate: 15/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 700, endDist: 750, slope: 0}, + {section: 16, phase: Phase.MidRace, endRate: 16/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 750, endDist: 800, slope: 0}, + {section: 17, phase: Phase.LateRace, endRate: 17/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 800, endDist: 850, slope: 0}, + {section: 18, phase: Phase.LateRace, endRate: 18/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 850, endDist: 900, slope: 0}, + {section: 19, phase: Phase.LateRace, endRate: 19/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 900, endDist: 950, slope: 0}, + {section: 20, phase: Phase.LateRace, endRate: 20/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 950, endDist: 1000, slope: 0}, + {section: 21, phase: Phase.LateRace, endRate: 21/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1000, endDist: 1050, slope: 0}, + {section: 22, phase: Phase.LateRace, endRate: 22/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1050, endDist: 1100, slope: 0}, + {section: 23, phase: Phase.LateRace, endRate: 23/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1100, endDist: 1150, slope: 0}, + {section: 24, phase: Phase.LateRace, endRate: 24/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1150, endDist: 1200, slope: 0}, + ], + }, + { + raceLen: 1200, + slopes: [{per: 1, span: [0, 50]}], + want: [ + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 0, endDist: 50, slope: 1}, + {section: 2, phase: Phase.EarlyRace, endRate: 2/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 50, endDist: 100, slope: 0}, + {section: 3, phase: Phase.EarlyRace, endRate: 3/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 100, endDist: 150, slope: 0}, + {section: 4, phase: Phase.EarlyRace, endRate: 4/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 150, endDist: 200, slope: 0}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 200, endDist: 250, slope: 0}, + {section: 6, phase: Phase.MidRace, endRate: 6/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 250, endDist: 300, slope: 0}, + {section: 7, phase: Phase.MidRace, endRate: 7/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 300, endDist: 350, slope: 0}, + {section: 8, phase: Phase.MidRace, endRate: 8/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 350, endDist: 400, slope: 0}, + {section: 9, phase: Phase.MidRace, endRate: 9/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 400, endDist: 450, slope: 0}, + {section: 10, phase: Phase.MidRace, endRate: 10/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 450, endDist: 500, slope: 0}, + {section: 11, phase: Phase.MidRace, endRate: 11/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 500, endDist: 550, slope: 0}, + {section: 12, phase: Phase.MidRace, endRate: 12/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 550, endDist: 600, slope: 0}, + {section: 13, phase: Phase.MidRace, endRate: 13/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 600, endDist: 650, slope: 0}, + {section: 14, phase: Phase.MidRace, endRate: 14/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 650, endDist: 700, slope: 0}, + {section: 15, phase: Phase.MidRace, endRate: 15/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 700, endDist: 750, slope: 0}, + {section: 16, phase: Phase.MidRace, endRate: 16/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 750, endDist: 800, slope: 0}, + {section: 17, phase: Phase.LateRace, endRate: 17/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 800, endDist: 850, slope: 0}, + {section: 18, phase: Phase.LateRace, endRate: 18/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 850, endDist: 900, slope: 0}, + {section: 19, phase: Phase.LateRace, endRate: 19/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 900, endDist: 950, slope: 0}, + {section: 20, phase: Phase.LateRace, endRate: 20/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 950, endDist: 1000, slope: 0}, + {section: 21, phase: Phase.LateRace, endRate: 21/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1000, endDist: 1050, slope: 0}, + {section: 22, phase: Phase.LateRace, endRate: 22/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1050, endDist: 1100, slope: 0}, + {section: 23, phase: Phase.LateRace, endRate: 23/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1100, endDist: 1150, slope: 0}, + {section: 24, phase: Phase.LateRace, endRate: 24/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1150, endDist: 1200, slope: 0}, + ], + }, + { + raceLen: 1200, + slopes: [{per: 1, span: [0, 225]}], + want: [ + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 0, endDist: 50, slope: 1}, + {section: 2, phase: Phase.EarlyRace, endRate: 2/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 50, endDist: 100, slope: 1}, + {section: 3, phase: Phase.EarlyRace, endRate: 3/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 100, endDist: 150, slope: 1}, + {section: 4, phase: Phase.EarlyRace, endRate: 4/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 150, endDist: 200, slope: 1}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 200, endDist: 225, slope: 1}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 225, endDist: 250, slope: 0}, + {section: 6, phase: Phase.MidRace, endRate: 6/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 250, endDist: 300, slope: 0}, + {section: 7, phase: Phase.MidRace, endRate: 7/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 300, endDist: 350, slope: 0}, + {section: 8, phase: Phase.MidRace, endRate: 8/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 350, endDist: 400, slope: 0}, + {section: 9, phase: Phase.MidRace, endRate: 9/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 400, endDist: 450, slope: 0}, + {section: 10, phase: Phase.MidRace, endRate: 10/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 450, endDist: 500, slope: 0}, + {section: 11, phase: Phase.MidRace, endRate: 11/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 500, endDist: 550, slope: 0}, + {section: 12, phase: Phase.MidRace, endRate: 12/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 550, endDist: 600, slope: 0}, + {section: 13, phase: Phase.MidRace, endRate: 13/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 600, endDist: 650, slope: 0}, + {section: 14, phase: Phase.MidRace, endRate: 14/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 650, endDist: 700, slope: 0}, + {section: 15, phase: Phase.MidRace, endRate: 15/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 700, endDist: 750, slope: 0}, + {section: 16, phase: Phase.MidRace, endRate: 16/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 750, endDist: 800, slope: 0}, + {section: 17, phase: Phase.LateRace, endRate: 17/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 800, endDist: 850, slope: 0}, + {section: 18, phase: Phase.LateRace, endRate: 18/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 850, endDist: 900, slope: 0}, + {section: 19, phase: Phase.LateRace, endRate: 19/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 900, endDist: 950, slope: 0}, + {section: 20, phase: Phase.LateRace, endRate: 20/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 950, endDist: 1000, slope: 0}, + {section: 21, phase: Phase.LateRace, endRate: 21/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1000, endDist: 1050, slope: 0}, + {section: 22, phase: Phase.LateRace, endRate: 22/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1050, endDist: 1100, slope: 0}, + {section: 23, phase: Phase.LateRace, endRate: 23/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1100, endDist: 1150, slope: 0}, + {section: 24, phase: Phase.LateRace, endRate: 24/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1150, endDist: 1200, slope: 0}, + ], + }, + { + raceLen: 1200, + slopes: [{per: 1, span: [25, 50]}], + want: [ + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 0, endDist: 25, slope: 0}, + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 25, endDist: 50, slope: 1}, + {section: 2, phase: Phase.EarlyRace, endRate: 2/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 50, endDist: 100, slope: 0}, + {section: 3, phase: Phase.EarlyRace, endRate: 3/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 100, endDist: 150, slope: 0}, + {section: 4, phase: Phase.EarlyRace, endRate: 4/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 150, endDist: 200, slope: 0}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 200, endDist: 250, slope: 0}, + {section: 6, phase: Phase.MidRace, endRate: 6/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 250, endDist: 300, slope: 0}, + {section: 7, phase: Phase.MidRace, endRate: 7/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 300, endDist: 350, slope: 0}, + {section: 8, phase: Phase.MidRace, endRate: 8/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 350, endDist: 400, slope: 0}, + {section: 9, phase: Phase.MidRace, endRate: 9/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 400, endDist: 450, slope: 0}, + {section: 10, phase: Phase.MidRace, endRate: 10/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 450, endDist: 500, slope: 0}, + {section: 11, phase: Phase.MidRace, endRate: 11/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 500, endDist: 550, slope: 0}, + {section: 12, phase: Phase.MidRace, endRate: 12/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 550, endDist: 600, slope: 0}, + {section: 13, phase: Phase.MidRace, endRate: 13/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 600, endDist: 650, slope: 0}, + {section: 14, phase: Phase.MidRace, endRate: 14/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 650, endDist: 700, slope: 0}, + {section: 15, phase: Phase.MidRace, endRate: 15/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 700, endDist: 750, slope: 0}, + {section: 16, phase: Phase.MidRace, endRate: 16/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 750, endDist: 800, slope: 0}, + {section: 17, phase: Phase.LateRace, endRate: 17/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 800, endDist: 850, slope: 0}, + {section: 18, phase: Phase.LateRace, endRate: 18/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 850, endDist: 900, slope: 0}, + {section: 19, phase: Phase.LateRace, endRate: 19/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 900, endDist: 950, slope: 0}, + {section: 20, phase: Phase.LateRace, endRate: 20/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 950, endDist: 1000, slope: 0}, + {section: 21, phase: Phase.LateRace, endRate: 21/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1000, endDist: 1050, slope: 0}, + {section: 22, phase: Phase.LateRace, endRate: 22/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1050, endDist: 1100, slope: 0}, + {section: 23, phase: Phase.LateRace, endRate: 23/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1100, endDist: 1150, slope: 0}, + {section: 24, phase: Phase.LateRace, endRate: 24/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1150, endDist: 1200, slope: 0}, + ], + }, + { + raceLen: 1200, + slopes: [{per: 1, span: [10, 20]}, {per: -1, span: [30, 40]}], + want: [ + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 0, endDist: 10, slope: 0}, + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 10, endDist: 20, slope: 1}, + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 20, endDist: 30, slope: 0}, + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 30, endDist: 40, slope: -1}, + {section: 1, phase: Phase.EarlyRace, endRate: 1/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 40, endDist: 50, slope: 0}, + {section: 2, phase: Phase.EarlyRace, endRate: 2/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 50, endDist: 100, slope: 0}, + {section: 3, phase: Phase.EarlyRace, endRate: 3/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 100, endDist: 150, slope: 0}, + {section: 4, phase: Phase.EarlyRace, endRate: 4/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 150, endDist: 200, slope: 0}, + {section: 5, phase: Phase.MidRace, endRate: 5/24, posKeep: true, rush: true, spotStruggleEnter: true, startDist: 200, endDist: 250, slope: 0}, + {section: 6, phase: Phase.MidRace, endRate: 6/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 250, endDist: 300, slope: 0}, + {section: 7, phase: Phase.MidRace, endRate: 7/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 300, endDist: 350, slope: 0}, + {section: 8, phase: Phase.MidRace, endRate: 8/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 350, endDist: 400, slope: 0}, + {section: 9, phase: Phase.MidRace, endRate: 9/24, posKeep: true, rush: true, spotStruggleEnter: false, startDist: 400, endDist: 450, slope: 0}, + {section: 10, phase: Phase.MidRace, endRate: 10/24, posKeep: true, rush: false, spotStruggleEnter: false, startDist: 450, endDist: 500, slope: 0}, + {section: 11, phase: Phase.MidRace, endRate: 11/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 500, endDist: 550, slope: 0}, + {section: 12, phase: Phase.MidRace, endRate: 12/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 550, endDist: 600, slope: 0}, + {section: 13, phase: Phase.MidRace, endRate: 13/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 600, endDist: 650, slope: 0}, + {section: 14, phase: Phase.MidRace, endRate: 14/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 650, endDist: 700, slope: 0}, + {section: 15, phase: Phase.MidRace, endRate: 15/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 700, endDist: 750, slope: 0}, + {section: 16, phase: Phase.MidRace, endRate: 16/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 750, endDist: 800, slope: 0}, + {section: 17, phase: Phase.LateRace, endRate: 17/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 800, endDist: 850, slope: 0}, + {section: 18, phase: Phase.LateRace, endRate: 18/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 850, endDist: 900, slope: 0}, + {section: 19, phase: Phase.LateRace, endRate: 19/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 900, endDist: 950, slope: 0}, + {section: 20, phase: Phase.LateRace, endRate: 20/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 950, endDist: 1000, slope: 0}, + {section: 21, phase: Phase.LateRace, endRate: 21/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1000, endDist: 1050, slope: 0}, + {section: 22, phase: Phase.LateRace, endRate: 22/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1050, endDist: 1100, slope: 0}, + {section: 23, phase: Phase.LateRace, endRate: 23/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1100, endDist: 1150, slope: 0}, + {section: 24, phase: Phase.LateRace, endRate: 24/24, posKeep: false, rush: false, spotStruggleEnter: false, startDist: 1150, endDist: 1200, slope: 0}, + ], + }, +])('splitSectionSlopes($raceLen, $slopes)', ({raceLen, slopes, want}) => { + expect(course.splitSectionSlopes(raceLen, slopes)).toEqual(want); +}) \ No newline at end of file diff --git a/zenno/src/lib/course.ts b/zenno/src/lib/course.ts new file mode 100644 index 0000000..6aa87a1 --- /dev/null +++ b/zenno/src/lib/course.ts @@ -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 = [ + {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); + }) +} diff --git a/zenno/src/routes/race/stam/+page.svelte b/zenno/src/routes/race/stam/+page.svelte new file mode 100644 index 0000000..80f6f56 --- /dev/null +++ b/zenno/src/routes/race/stam/+page.svelte @@ -0,0 +1,86 @@ + \ No newline at end of file