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
+195
View File
@@ -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);
})
+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);
})
}
+86
View File
@@ -0,0 +1,86 @@
<script lang="ts">
import * as course from '$lib/course';
import * as race from '$lib/race';
let rawStats = $state({
[race.Stat.Speed]: 1200,
[race.Stat.Stamina]: 1200,
[race.Stat.Power]: 1200,
[race.Stat.Guts]: 1200,
[race.Stat.Wit]: 1200,
});
let style = $state(race.RunningStyle.FrontRunner);
let surfaceApt = $state(race.AptitudeLevel.A);
let distanceApt = $state(race.AptitudeLevel.S);
let styleApt = $state(race.AptitudeLevel.A);
let mood = $state(race.Mood.Normal);
let isCareer = $state(false);
let skillStatBonus = $state({
[race.Stat.Speed]: 0,
[race.Stat.Stamina]: 0,
[race.Stat.Power]: 0,
[race.Stat.Guts]: 0,
[race.Stat.Wit]: 0,
});
let raceLen = $state(2000);
let slopesRaw: course.Slope[] = $state([]);
let surface = $state(race.Surface.Turf);
let conditions = $state(race.GroundConditions.Firm);
let thresh1: race.Stat | undefined = $state();
let thresh2: race.Stat | undefined = $state();
const baseStats = $derived({
[race.Stat.Speed]: race.baseStat(mood, rawStats[race.Stat.Speed]),
[race.Stat.Stamina]: race.baseStat(mood, rawStats[race.Stat.Stamina]),
[race.Stat.Power]: race.baseStat(mood, rawStats[race.Stat.Power]),
[race.Stat.Guts]: race.baseStat(mood, rawStats[race.Stat.Guts]),
[race.Stat.Wit]: race.baseStat(mood, rawStats[race.Stat.Wit]),
});
const thresholdMod = $derived.by(() => {
if (thresh1 == null) {
if (thresh2 == null) {
return 1;
}
return race.thresholdMod(baseStats[thresh2]);
}
if (thresh2 == null) {
return race.thresholdMod(baseStats[thresh1]);
}
return race.thresholdMod(baseStats[thresh1], baseStats[thresh2]);
});
const adjStats = $derived({
[race.Stat.Speed]: race.adjustedStat(race.Stat.Speed, baseStats[race.Stat.Speed], isCareer, surface, conditions, thresholdMod),
[race.Stat.Stamina]: race.adjustedStat(race.Stat.Stamina, baseStats[race.Stat.Stamina], isCareer),
[race.Stat.Power]: race.adjustedStat(race.Stat.Power, baseStats[race.Stat.Power], isCareer, surface, conditions),
[race.Stat.Guts]: race.adjustedStat(race.Stat.Guts, baseStats[race.Stat.Guts], isCareer),
[race.Stat.Wit]: race.adjustedStat(race.Stat.Wit, baseStats[race.Stat.Wit], isCareer, styleApt),
});
const stats = $derived({
[race.Stat.Speed]: race.finalStat(adjStats[race.Stat.Speed], skillStatBonus[race.Stat.Speed]),
[race.Stat.Stamina]: race.finalStat(adjStats[race.Stat.Stamina], skillStatBonus[race.Stat.Stamina]),
[race.Stat.Power]: race.finalStat(adjStats[race.Stat.Power], skillStatBonus[race.Stat.Power]),
[race.Stat.Guts]: race.finalStat(adjStats[race.Stat.Guts], skillStatBonus[race.Stat.Guts]),
[race.Stat.Wit]: race.finalStat(adjStats[race.Stat.Wit], skillStatBonus[race.Stat.Wit]),
});
let pdmTime = $state(0);
let rushedEnabled = $state(true);
let spotStruggleEnabled = $state(true);
const phases = $derived(course.phases(raceLen));
const sectionSlopes = $derived(course.splitSectionSlopes(raceLen, slopesRaw));
const sectionLen = $derived(raceLen / course.SECTIONS.length);
const phaseSpeed = $derived({
[race.Phase.EarlyRace]: race.sectionSpeed(raceLen, baseStats[race.Stat.Wit], stats[race.Stat.Wit], style, race.Phase.EarlyRace),
[race.Phase.MidRace]: race.sectionSpeed(raceLen, baseStats[race.Stat.Wit], stats[race.Stat.Wit], style, race.Phase.MidRace),
});
const spurtSpeed = $derived(race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen));
const spurtHPRate = $derived(race.hpPerSecond(raceLen, surface, conditions, stats[race.Stat.Guts], race.Phase.LateRace, spurtSpeed));
const needHP = $derived(race.fullSpurtHP(raceLen, spurtSpeed, spurtHPRate));
</script>