From b1c6850ce106d24292de840c430f0fc5876a8d08 Mon Sep 17 00:00:00 2001
From: Branden J Brown
Date: Sun, 24 May 2026 17:55:57 -0400
Subject: [PATCH] zenno/doc/frbm: chart and calcs for section speed
---
zenno/src/lib/StatChart.svelte | 15 ++-
zenno/src/lib/chart.ts | 6 ++
zenno/src/lib/race.ts | 64 ++++++++++++
zenno/src/routes/doc/frbm/+page.svelte | 134 ++++++++++++++++++++-----
4 files changed, 191 insertions(+), 28 deletions(-)
diff --git a/zenno/src/lib/StatChart.svelte b/zenno/src/lib/StatChart.svelte
index 00510bc..eb4d6b7 100644
--- a/zenno/src/lib/StatChart.svelte
+++ b/zenno/src/lib/StatChart.svelte
@@ -2,7 +2,7 @@
import * as Plot from '@observablehq/plot';
import * as d3 from 'd3';
import { Stat } from './race';
- import type { ComputedSeries, HorizontalRule } from './chart';
+ import type { ComputedAreas, ComputedSeries, HorizontalRule } from './chart';
import type { ClassValue } from 'svelte/elements';
import type { Attachment } from 'svelte/attachments';
@@ -10,7 +10,9 @@
/** The stat the chart shows. */
stat: Stat;
/** Series to show in the chart. */
- y: ComputedSeries | Array;
+ y?: ComputedSeries | Array;
+ /** Areas to show in the chart. */
+ yArea?: ComputedAreas | Array;
/** Label for the dependent variable. */
yLabel: string;
/**
@@ -37,7 +39,7 @@
plotOptions?: Omit;
}
- let { stat, y, yLabel, range, xRange, xRule = [], yRule = [], class: className, plotOptions = {} }: Props = $props();
+ let { stat, y = [], yArea = [], yLabel, range, xRange, xRule = [], yRule = [], class: className, plotOptions = {} }: Props = $props();
let width = $state(0);
let height = $state(0);
@@ -60,7 +62,9 @@
const thrX = 1200;
const series = $derived([y].flat(1).filter((s) => s != null));
+ const areas = $derived([yArea].flat(1).filter((s) => s != null));
const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(x), label }))));
+ const areaVals = $derived(areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(x), y2: y2(x), label }))));
const yRange: [number, number] = $derived.by(() => {
if (range != null) {
if (range.length === 2) {
@@ -103,8 +107,11 @@
Plot.ruleY(yRule, { y: 'y', strokeOpacity: 0.75 }),
Plot.tip(yRule, { x: xMax, y: 'y', title: 'label', anchor: 'top-right', className: 'plot-tip' }),
Plot.frame(),
+ Plot.areaY(areaVals, { x: 'x', y1: 'y1', y2: 'y2', fill: 'label', fillOpacity: 0.5 }),
+ Plot.line(areaVals, { x: 'x', y: 'y1', stroke: 'label' }),
+ Plot.line(areaVals, { x: 'x', y: 'y2', stroke: 'label' }),
Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
- Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })),
+ vals.length > 0 ? Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })) : null,
],
}),
);
diff --git a/zenno/src/lib/chart.ts b/zenno/src/lib/chart.ts
index e97530b..d95c04b 100644
--- a/zenno/src/lib/chart.ts
+++ b/zenno/src/lib/chart.ts
@@ -3,6 +3,12 @@ export interface ComputedSeries {
label: string;
}
+export interface ComputedAreas {
+ y1: (x: number) => number;
+ y2: (x: number) => number;
+ label: string;
+}
+
export interface HorizontalRule {
y: number;
label: string;
diff --git a/zenno/src/lib/race.ts b/zenno/src/lib/race.ts
index 859176c..c464bab 100644
--- a/zenno/src/lib/race.ts
+++ b/zenno/src/lib/race.ts
@@ -1,6 +1,7 @@
// Umamusume race mechanics adapted from KuromiAK's doc:
// https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?usp=sharing
+import * as math from "mathjs";
import { binomPMF } from "./prob";
/**
@@ -43,6 +44,17 @@ export enum RunningStyle {
GreatEscape,
}
+/**
+ * Running styles with their proper names, for easy iterating.
+ */
+export const RUNNING_STYLES = [
+ ['Front Runner', RunningStyle.FrontRunner],
+ ['Pace Chaser', RunningStyle.PaceChaser],
+ ['Late Surger', RunningStyle.LateSurger],
+ ['End Closer', RunningStyle.EndCloser],
+ ['Great Escape', RunningStyle.GreatEscape],
+] as const;
+
/**
* Aptitude or proficiency levels.
*/
@@ -95,6 +107,40 @@ const speedStrategyPhaseCoeff = [
const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as const;
+/**
+ * Calculate the range of section speed values for a horse in early or mid race.
+ * @param raceLen Length of the race in meters
+ * @param baseWit Base wit, after mood but before other modifiers
+ * @param witStat Final wit stat, including strategy proficiency and skills
+ * @param style Horse's running style
+ * @param phase Phase of the current section
+ */
+export function sectionSpeed(raceLen: number, baseWit: number, witStat: number, style: RunningStyle, phase: Exclude): [number, number];
+/**
+ * Calculate the range of section speed values for a horse not spurting during late race.
+ * @param raceLen Length of the race in meters
+ * @param speedStat Final speed stat
+ * @param baseWit Base wit, after mood but before other modifiers
+ * @param witStat Final wit stat, including strategy proficiency and skills
+ * @param style Horse's running style
+ * @param distance Hores's distance proficiency aptitude
+ * @param phase Phase.LateRace
+ */
+export function sectionSpeed(raceLen: number, speedStat: number, baseWit: number, witStat: number, style: RunningStyle, distance: AptitudeLevel, phase: Phase.LateRace): [number, number];
+export function sectionSpeed(raceLen: number, speedStatOrBaseWit: number, baseWitOrWitStat: number, witStatOrStyle: number | RunningStyle, styleOrPhase: RunningStyle | Phase, distance?: AptitudeLevel, lateRace?: Phase.LateRace): [number, number] {
+ const speedStat = lateRace !== undefined ? speedStatOrBaseWit : 0;
+ const baseWit = lateRace !== undefined ? baseWitOrWitStat : speedStatOrBaseWit;
+ const witStat = lateRace !== undefined ? witStatOrStyle : baseWitOrWitStat;
+ const style = lateRace !== undefined ? styleOrPhase as RunningStyle : witStatOrStyle as RunningStyle;
+ const phase = lateRace !== undefined ? lateRace : styleOrPhase as Phase;
+ const base = baseSpeed(raceLen);
+ const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
+ const late = phase === Phase.LateRace ? (math.sqrt(500 * speedStat) as number) * distanceProficiencyMod[distance ?? AptitudeLevel.A] * 0.002 : 0;
+ const u = witStat / 550000 * math.log10(baseWit * 0.1);
+ const l = u - 0.0065;
+ return [baseTarget + late + base*l, baseTarget + late + base*u];
+}
+
/**
* Calculate an Uma's last spurt target speed.
* @param speedStat Adjusted speed stat
@@ -326,3 +372,21 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null,
export function downhillAccelEnterChance(witStat: number): number {
return witStat * 0.0004;
}
+
+/**
+ * Calculate the chance for a front runner to enter speed-up or overtake mode when eligible.
+ * @param witStat Final wit stat, including style aptitude modifier
+ * @returns Probability each eligible tick to enter speed-up or overtake mode
+ */
+export function frontModeEnterChance(witStat: number): number {
+ return 0.2 * math.log10(witStat) - 0.2;
+}
+
+/**
+ * Calculate the chance for a non-front runner to enter pace-up mode when eligible.
+ * @param witStat Final wit stat, including style aptitude modifier
+ * @returns Probability each eligible tick to enter pace-up mode
+ */
+export function paceUpEnterChance(witStat: number): number {
+ return 0.15 * math.log10(witStat) - 0.15;
+}
diff --git a/zenno/src/routes/doc/frbm/+page.svelte b/zenno/src/routes/doc/frbm/+page.svelte
index 87d7a7d..141be44 100644
--- a/zenno/src/routes/doc/frbm/+page.svelte
+++ b/zenno/src/routes/doc/frbm/+page.svelte
@@ -1,15 +1,44 @@
@@ -63,9 +123,11 @@
That said, most of the information here is ultimately my interpretations of KuromiAK's Race Mechanics dockuromiAK's Race Mechanics doc. Many of those interpretations are also informed by the exceptionally knowledgeable folks on the
GameTora Discord server.
+ I may present some of the information from the race mechanics doc in chart form, but I will generally leave out exact mechanic numbers and conditions;
+ the doc is already the place for that information.
I want to share the knowledge I've accrued about front runners, because teaching is my favorite thing. Definitely not just to
@@ -110,7 +172,7 @@
Win Conditions
On Global today, competitive horses usually have stat lines that are pretty similar to each other.
- Races, therefore, are more often won by skills – typically acceleration skills that activate at the start of late race.
+ Races, therefore, are more often won by skills – typically acceleration skills that activate at the start of late race.
Front runners have strong options.
@@ -154,6 +216,16 @@
Their mid race speed skills always gain distance.
+ Speed-Up and Overtake Modes
+
+ Instead of pace-down and pace-up, front runners have speed-up (+4% target speed for first among that front type) and overtake (+5% for not-first) modes.
+ Entering these modes requires meeting certain conditions relating to positioning, which collectively can be read as "solo fronts are heavily penalized."
+ They also require passing a wit check, with the same chance for both modes.
+
+
+
+
+
Skill Timing
Thought experiment.
@@ -200,12 +272,12 @@
For reference, the chart below shows proc chances of one of one, one of two, or two of two skills with wit checks.
-
+
- TTL must be combined with GW if they want any chance of being first out of early race.
- Since the main source of it is the Mihono Bourbon Wit SSR from the first Halloween event, VBourbon can suffice with its white version and get to the front with her unique instead.
- (Her other option is the Twin Turbo SSR that does generate a lot of stats but requires winning three 50/50s to get the gold skill.)
+ TTL or its white version must be combined with GW if they want any chance of being first out of early race.
+ Since the main source of TTL is the Mihono Bourbon Wit SSR from the first Halloween event, VBourbon can suffice with EL.
+ (The other TTL option is the Twin Turbo SSR that does generate a lot of stats but requires winning three 50/50s to get the gold skill.)
Conc is less critical.
@@ -320,18 +392,32 @@
The modifier's range is determined by the wit stat.
(Curiously, the calculation uses both wit as modified by style proficiency and green skills as well as base wit.)
+
+
+
+
+
+ {raceLen}m
+
+
+
+
- Section speed is generally very small; at 1200 wit with style S, it has a range of about -0.15% to 0.5% of race base speed.
- At 2000m, that translates to an actual speed range of 19.97 to 20.10 m/s.
+ Section speed is generally very small; at {secSpeedS} wit with style S, it has a range of about -0.15% to 0.5% of race base speed.
+ For a front runner at {raceLen}m, that translates to an actual speed range of {secSpeedInfo} m/s.
- Unlike anything affected by the speed stat, though, it applies during the early and mid race, where front runners are trying to become frontest runners.
- Wit difference alone can
+ It isn't negligible, though, since it applies during the portion of the race where front runners are trying to become frontest runners.
+ All else equal, including the effects of running modes, such a horse blocked in front by a {secSpeedA} wit front A horse will pass in {secSpeedPassTime} seconds on average at mid race speeds.
Phase Speed
- Race base speed is multiplied by the strategy–phase coefficient for each horse.
+ Race base speed is multiplied by the strategy–phase coefficient for each horse.
As the name suggests, SPC is different per running style and per race phase.
It's the thing that makes runaways take off in early race, and the thing that makes pace chaser promotion scary in late race (for those not using any of the correct running style).
@@ -358,9 +444,9 @@
Lane Combo
- Speed Skills – Front Runner Exclusives
+ Speed Skills – Front Runner Exclusives
- Speed Skills – Generic
+ Speed Skills – GenericSpurt Skills
@@ -503,7 +589,7 @@
with your intended style, but in career, winning is more important than front running.
My CM Teams
- CM13 – Taurus Cup (Tokyo Derby)
+ CM13 – Taurus Cup (Tokyo Derby)
Maruzensky's unique is live as an order≤5 for approximately everyone.
Filling the ranks with front runners should be a strong means to delay it for later positions, especially COC.
@@ -556,7 +642,7 @@
If you prefer winning over running your favorites, this should be Maruzensky instead.
- CM12 – Aries Cup (Satsuki Sho)
+ CM12 – Aries Cup (Satsuki Sho)
One of COC's best tracks, because U=ma2 is at worst only slightly less good than 777 as a trigger.
If there is any other front runner, triple front pushes pace COC out of range for U=ma2, making her at best as reliable as the usual.
@@ -593,7 +679,7 @@
Win rates after 80: VBourbon 30%, Sei 22.5%, Suzuka 12.5%. I believe this is my best round 2 performance ever.
I lose more to other fronts than to COC. "Most dominant racing horse for a year" continues to get trounced by the wacky triple front build.
- CM11 – Pisces Cup (Hanshin 3200 Heavy Rain)
+ CM11 – Pisces Cup (Hanshin 3200 Heavy Rain)
N.B. This CM was before I started writing this document, so henceforth, there is much less info.
@@ -625,7 +711,7 @@
Extremely unlucky finals gave me third place for the first time ever.
- CM10 – Aquarius Cup (February Stakes)
+ CM10 – Aquarius Cup (February Stakes)
Everyone is terrified of Taiki Shuttle, who has a 3-4 ult.
Triple fronts would like to have a word.
@@ -634,7 +720,7 @@
Smart Falcon is the obvious choice, being the only actual dirt front runner to exist.
- Her unique isn't terribly strong for this track, but her gold skills are – Trending makes it extremely difficult for others to overtake her.
+ Her unique isn't terribly strong for this track, but her gold skills are – Trending makes it extremely difficult for others to overtake her.
1200/467/920/410/930 A/S/A.