Skip to content

Commit

Permalink
estimate future segments based off PB
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-vorobiov committed Dec 22, 2024
1 parent 9dceb72 commit 60279d0
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 130 deletions.
2 changes: 1 addition & 1 deletion evolve_analytics.meta.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Evolve Analytics
// @namespace http://tampermonkey.net/
// @version 0.10.0
// @version 0.10.1
// @description Track and see detailed information about your runs
// @author Sneed
// @match https://pmotschmann.github.io/Evolve/
Expand Down
43 changes: 43 additions & 0 deletions src/exports/historyFiltering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,46 @@ export function applyFilters(history: HistoryManager, view: ViewConfig): History

return runs.reverse();
}

function findBestRunImpl(history: HistoryManager, view: ViewConfig): HistoryEntry | undefined {
let best: HistoryEntry | undefined = undefined;

for (let i = 0; i != history.runs.length; ++i) {
const run = history.runs[i];

if (!shouldIncludeRun(run, view, history)) {
continue;
}

if (best === undefined) {
best = run;
}
else {
const [, bestTime] = best.milestones[best.milestones.length - 1];
const [, currentTime] = run.milestones[run.milestones.length - 1];

if (currentTime < bestTime) {
best = run;
}
}
}

return best;
}

const bestRunCache: Record<string, HistoryEntry> = {};

export function findBestRun(history: HistoryManager, view: ViewConfig): HistoryEntry | undefined {
const cacheKey = `${view.resetType}.${view.universe ?? "*"}`;
const cacheEntry = bestRunCache[cacheKey];
if (cacheEntry !== undefined) {
return cacheEntry;
}

const best = findBestRunImpl(history, view);
if (best !== undefined) {
bestRunCache[cacheKey] = best;
}

return best;
}
140 changes: 105 additions & 35 deletions src/exports/plotPoints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateMilestoneNames } from "../milestones";
import { zip } from "../utils";
import { generateMilestoneNames, isEventMilestone } from "../milestones";
import { zip, rotateMap } from "../utils";
import type { HistoryManager, HistoryEntry } from "../history";
import type { ViewConfig } from "../config";
import type { LatestRun } from "../runTracking";
Expand All @@ -11,7 +11,9 @@ export type PlotPoint = {
dayDiff?: number, // days since the last enabled non-event milestone
segment: number, // days since the last non-event milestone
raceName?: string,
pending?: boolean
pending?: boolean,
future?: boolean,
overtime?: boolean
}

function makeMilestoneNamesMapping(history: HistoryManager, view: ViewConfig): Record<number, string> {
Expand All @@ -37,7 +39,7 @@ class SegmentCounter {
const dayDiff = day - this.previousEnabledDay;
const segment = day - this.previousDay;

const isEvent = milestone.startsWith("event:");
const isEvent = isEventMilestone(milestone);
const enabled = this.view.milestones[milestone];

if (!isEvent) {
Expand All @@ -57,30 +59,42 @@ class SegmentCounter {
}
}

export function runAsPlotPoints(currentRun: LatestRun, view: ViewConfig, runIdx: number, orderedMilestones: string[]): PlotPoint[] {
const milestoneNames = generateMilestoneNames(orderedMilestones, view.universe);
export function runAsPlotPoints(
currentRun: LatestRun,
view: ViewConfig,
bestRun: PlotPoint[] | undefined,
estimateFutureMilestones: boolean,
runIdx: number
): PlotPoint[] {
const onlyRun = bestRun === undefined || bestRun.length === 0;

const entries: PlotPoint[] = [];
const milestones = Object.keys(view.milestones);
const milestoneNames = generateMilestoneNames(milestones, view.universe);
const milestoneNameMap = Object.fromEntries(zip(milestones, milestoneNames)) as Record<string, string>;

const counter = new SegmentCounter(view);
const bestRunTimes = onlyRun ? {} : Object.fromEntries(bestRun.map(entry => [entry.milestone, entry.day]));
let offset = 0;

let nextMilestoneIdx = 0;
for (let i = 0; i !== orderedMilestones.length; ++i) {
const milestone = orderedMilestones[i];
const milestoneName = milestoneNames[i];
const entries: PlotPoint[] = [];
let counter = new SegmentCounter(view);

const day = currentRun.milestones[milestone];
if (day === undefined) {
for (const [milestone, day] of Object.entries(currentRun.milestones)) {
if (!(milestone in view.milestones)) {
continue;
}

nextMilestoneIdx = i + 1;
const milestoneName = milestoneNameMap[milestone];

const info = counter.next(milestone, day);
if (info === undefined) {
continue;
}

// Difference between the last common non-event milestone
if (milestoneName in bestRunTimes && !isEventMilestone(milestone)) {
offset = day - bestRunTimes[milestoneName];
}

entries.push({
run: runIdx,
raceName: currentRun.raceName,
Expand All @@ -91,30 +105,86 @@ export function runAsPlotPoints(currentRun: LatestRun, view: ViewConfig, runIdx:
});
}

// Guess what the next milestone is gonna be, default to the view's reset
let milestone = `reset:${view.resetType}`;
for (; nextMilestoneIdx !== orderedMilestones.length; ++nextMilestoneIdx) {
const candidate = orderedMilestones[nextMilestoneIdx];
if (!candidate.startsWith("event:") && view.milestones[candidate]) {
milestone = candidate;
break;
if (onlyRun) {
const nextMilestone = `reset:${view.resetType}`;

const info = counter.next(nextMilestone, currentRun.totalDays);
if (info === undefined) {
return entries;
}
}

const info = counter.next(milestone, currentRun.totalDays);
if (info === undefined) {
return entries;
entries.push({
run: runIdx,
raceName: currentRun.raceName,
milestone: milestoneNameMap[nextMilestone],
day: currentRun.totalDays,
dayDiff: info.dayDiff,
segment: info.segment,
pending: true
});
}
else {
const reverseMilestoneNameMap = rotateMap(milestoneNameMap);

let idx = 0;
if (entries.length !== 0) {
idx = bestRun.findLastIndex(entry => entry.milestone === entries[entries.length - 1].milestone);
}

entries.push({
run: runIdx,
raceName: currentRun.raceName,
milestone: generateMilestoneNames([milestone], view.universe)[0],
day: currentRun.totalDays,
dayDiff: info.dayDiff,
segment: info.segment,
pending: true
});
const futureEntries = bestRun.slice(idx).filter(entry => {
const milestone = reverseMilestoneNameMap[entry.milestone];
return !(milestone in currentRun.milestones) && !isEventMilestone(milestone);
});

if (futureEntries.length === 0) {
return entries;
}

// Current segment
const nextMilestone = reverseMilestoneNameMap[futureEntries[0].milestone];

const { dayDiff, segment } = counter.next(nextMilestone, currentRun.totalDays)!;

const overtime = segment >= futureEntries[0].segment;

entries.push({
run: runIdx,
raceName: currentRun.raceName,
milestone: futureEntries[0].milestone,
day: currentRun.totalDays,
dayDiff,
segment,
pending: true,
overtime
});

if (overtime) {
offset = currentRun.totalDays - futureEntries[0].day;
}

if (estimateFutureMilestones) {
for (const entry of futureEntries) {
const milestoneName = entry.milestone;
const milestone = reverseMilestoneNameMap[milestoneName];

const { dayDiff, segment } = counter.next(milestone, entry.day + offset)!;

if (segment === 0) {
continue;
}

entries.push({
run: runIdx,
raceName: currentRun.raceName,
milestone: entry.milestone,
day: entry.day + offset,
dayDiff,
segment,
future: true
});
}
}
}

return entries;
}
Expand Down
4 changes: 4 additions & 0 deletions src/milestones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ export function generateMilestoneNames(milestones: string[], universe?: keyof ty

return names;
}

export function isEventMilestone(milestone: string) {
return milestone.startsWith("event:");
}
23 changes: 17 additions & 6 deletions src/ui/graph.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { applyFilters } from "../exports/historyFiltering";
import { applyFilters, findBestRun } from "../exports/historyFiltering";
import { asPlotPoints, runAsPlotPoints, type PlotPoint } from "../exports/plotPoints";
import { generateMilestoneNames } from "../milestones";
import type { View } from "../config";
import type { HistoryEntry, HistoryManager } from "../history";
import type { LatestRun } from "../runTracking";

import type { default as PlotType } from "@observablehq/plot";
import type * as PlotType from "@observablehq/plot";

declare const Plot: typeof PlotType;

Expand Down Expand Up @@ -61,7 +61,7 @@ function smooth(smoothness: number, history: HistoryEntry[], params: any) {

function* timestamps(plotPoints: PlotPoint[], key: "day" | "segment") {
const lastRunTimestamps = lastRunEntries(plotPoints)
.filter(entry => !entry.pending)
.filter(entry => !entry.pending || entry.overtime)
.map(entry => entry[key]);

yield Plot.axisY(lastRunTimestamps, {
Expand All @@ -77,7 +77,7 @@ function* areaMarks(plotPoints: PlotPoint[], history: HistoryEntry[], smoothness
z: "milestone",
fill: "milestone",
fillOpacity: 0.5,
filter: (entry: PlotPoint) => entry.dayDiff !== undefined
filter: (entry: PlotPoint) => entry.dayDiff !== undefined && !entry.future
}));
}

Expand All @@ -98,7 +98,7 @@ function* barMarks(plotPoints: PlotPoint[], key: "dayDiff" | "segment") {
y: key,
z: "milestone",
fill: "milestone",
fillOpacity: 0.5,
fillOpacity: (entry: PlotPoint) => entry.future ? 0.25 : 0.5,
// Don't display event milestones as segments - use only the ticks
filter: (entry: PlotPoint) => entry.dayDiff !== undefined
});
Expand Down Expand Up @@ -144,6 +144,10 @@ function tipText(point: PlotPoint, key: "day" | "dayDiff" | "segment", history:
}
else {
suffix = `in ${point[key]} day(s)`;

if (point.future) {
suffix += ` (PB pace)`;
}
}

return `${prefix}: ${point.milestone} ${suffix}`;
Expand Down Expand Up @@ -214,7 +218,14 @@ export function makeGraph(history: HistoryManager, view: View, currentRun: Lates
const plotPoints = asPlotPoints(filteredRuns, history, view);

if (view.includeCurrentRun) {
const currentRunPoints = runAsPlotPoints(currentRun, view, filteredRuns.length, milestones.slice().reverse());
const bestRun = findBestRun(history, view);
const bestRunEntries = bestRun !== undefined ? asPlotPoints([bestRun], history, view) : [];

const estimate = view.mode === "timestamp";

const idx = filteredRuns.length;

const currentRunPoints = runAsPlotPoints(currentRun, view, bestRunEntries, estimate, idx);
plotPoints.push(...currentRunPoints);
}

Expand Down
8 changes: 6 additions & 2 deletions src/ui/viewTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export function makeViewTab(id: string, game: Game, view: View, config: ConfigMa
discardRunNode.attr("disabled", selectedRun === null ? "" : null);
}

function createGraph(view: View) {
return makeGraph(history, view, currentRun, onRunSelection);
}

const buttonsContainerNode = $(`<div style="display: flex; justify-content: space-between"></div>`)
.append(asImageNode)
.append(discardRunNode)
Expand All @@ -117,11 +121,11 @@ export function makeViewTab(id: string, game: Game, view: View, config: ConfigMa
.append(makeViewSettings(view).css("margin-bottom", "1em"))
.append(makeAdditionalInfoSettings(view).css("margin-bottom", "1em"))
.append(makeMilestoneSettings(view).css("margin-bottom", "1em"))
.append(makeGraph(history, view, currentRun, onRunSelection))
.append(createGraph(view))
.append(buttonsContainerNode);

function redrawGraph(updatedView: View) {
contentNode.find("figure:last").replaceWith(makeGraph(history, updatedView, currentRun, onRunSelection));
contentNode.find("figure:last").replaceWith(createGraph(updatedView));
}

config.on("viewUpdated", (updatedView) => {
Expand Down
Loading

0 comments on commit 60279d0

Please sign in to comment.