Avoid building a full rank map for pair updates

This commit is contained in:
Jack Williams 2026-03-07 21:56:09 +11:00
parent 32f5618366
commit dd080a3488
6 changed files with 47 additions and 13 deletions

View file

@ -8,7 +8,7 @@ import type { PluginDataStore } from '../storage/PluginDataStore';
import { ConfirmModal } from '../ui/ConfirmModal';
import { ALL_SENTINEL, ResetNoteModal } from '../ui/ResetNoteModal';
import {
computeRankMap,
computeRanksForAll,
updateCohortFrontmatter,
writeFrontmatterStatsForPlayer,
} from '../utils/FrontmatterStats';
@ -88,7 +88,7 @@ export async function resetNoteRating(
def.frontmatterOverrides,
);
const rankMap = computeRankMap(cohort);
const rankMap = computeRanksForAll(cohort);
// Write the reset note's own stats (rating, matches, wins, rank)
await writeFrontmatterStatsForPlayer(app, fm, cohort, file, noteId, rankMap);

View file

@ -189,7 +189,9 @@ export function computePlaceholders(
const sessionWithSigma = allWithSigma.filter(({ id }) => uniquePlayerIds.has(id));
const formatStabilityList = (entries: { id: string; sigma: number }[]) =>
entries.map((e) => `- ${idToName(e.id, data.idToPath)} (σ ${Math.round(e.sigma)})`).join('\n');
entries
.map((e) => `- ${idToName(e.id, data.idToPath)} (σ ${Math.round(e.sigma)})`)
.join('\n');
// Session participants only
const byStabilitySession = sessionWithSigma.slice().sort((a, b) => a.sigma - b.sigma);

View file

@ -22,7 +22,7 @@ import { CohortPicker } from './ui/CohortPicker';
import { ensureBaseCohortTarget } from './utils/EnsureBaseCohort';
import { ensureFolderCohortPath } from './utils/EnsureFolderCohort';
import { ensureUniqueIds } from './utils/EnsureUniqueIds';
import { computeRankMap, updateCohortFrontmatter } from './utils/FrontmatterStats';
import { computeRanksForAll, updateCohortFrontmatter } from './utils/FrontmatterStats';
import { debugWarn, setDebugLogging } from './utils/logger';
export default class GlickoPlugin extends Plugin {
@ -262,7 +262,7 @@ export default class GlickoPlugin extends Plugin {
});
if (files.length === 0) return;
const rankMap = computeRankMap(cohort);
const rankMap = computeRanksForAll(cohort);
updateCohortFrontmatter(
this.app,

View file

@ -9,7 +9,7 @@ import { ConfirmModal } from '../ui/ConfirmModal';
import { FolderSelectModal } from '../ui/FolderPicker';
import { FM_PROP_KEYS, renderStandardFmPropertyRow } from '../ui/FrontmatterPropertyRow';
import {
computeRankMap,
computeRanksForAll,
previewCohortFrontmatterPropertyUpdates,
updateCohortFrontmatter,
} from '../utils/FrontmatterStats';
@ -562,7 +562,7 @@ export default class GlickoSettingsTab extends PluginSettingTab {
const map = new Map<string, number>();
if (!cohort) return map;
if (key === 'rank') {
const rankMap = computeRankMap(cohort);
const rankMap = computeRanksForAll(cohort);
for (const [id, rank] of rankMap) map.set(id, rank);
} else if (key === 'rating') {
for (const [id, p] of Object.entries(cohort.players)) map.set(id, Math.round(p.rating));

View file

@ -214,7 +214,11 @@ export class PluginDataStore {
ts: now,
};
// Inflate sigma for staleness (Glicko-1 RD inflation)
// Inflate sigma for staleness (Glicko-1 RD inflation).
// Cap is intentionally stabilityThreshold, not DEFAULT_SIGMA: notes don't
// "lose skill" like human players, so we only allow modest RD inflation to
// accommodate preference drift. This also prevents the stability progress
// bar from regressing after long gaps between sessions.
const cap = this.settings.stabilityThreshold ?? 150;
const preSigmaA = inflateSigma(a.sigma ?? DEFAULT_SIGMA, now - (a.lastMatchAt ?? now), cap);
const preSigmaB = inflateSigma(b.sigma ?? DEFAULT_SIGMA, now - (b.lastMatchAt ?? now), cap);

View file

@ -17,7 +17,7 @@ function anyEnabled(fm: FrontmatterPropertiesSettings): boolean {
}
// Standard competition ranking ("1224" style)
export function computeRankMap(cohort: CohortData): Map<string, number> {
export function computeRanksForAll(cohort: CohortData): Map<string, number> {
const entries = Object.entries(cohort.players);
entries.sort((a, b) => b[1].rating - a[1].rating);
@ -38,6 +38,33 @@ export function computeRankMap(cohort: CohortData): Map<string, number> {
return map;
}
// Compute ranks for a subset of players by counting how many players
// have a strictly higher rating (standard competition ranking)
function computeRanksForSubset(cohort: CohortData, playerIds: string[]): Map<string, number> {
const targets = playerIds
.map((id) => ({ id, rating: cohort.players[id]?.rating }))
.filter((t): t is { id: string; rating: number } => t.rating !== undefined);
if (targets.length === 0) return new Map();
// For each player in the cohort, check if their rating exceeds
// any target's rating and increment that target's rank counter
const higherCounts = new Map<string, number>(targets.map((t) => [t.id, 0]));
for (const player of Object.values(cohort.players)) {
for (const t of targets) {
if (player.rating > t.rating) {
higherCounts.set(t.id, higherCounts.get(t.id)! + 1);
}
}
}
const rankMap = new Map<string, number>();
for (const [id, count] of higherCounts) {
rankMap.set(id, count + 1);
}
return rankMap;
}
function buildProps(fm: FrontmatterPropertiesSettings, stats: PlayerStats): Record<string, number> {
const out: Record<string, number> = {};
@ -78,7 +105,8 @@ export async function writeFrontmatterStatsForPair(
if (!cohort) return;
if (!anyEnabled(fm)) return;
const rankMap = computeRankMap(cohort);
const ids = [aId, bId].filter((id): id is string => !!id);
const rankMap = computeRanksForSubset(cohort, ids);
const tasks: Promise<void>[] = [];
if (aFile && aId) {
@ -258,7 +286,7 @@ export async function updateCohortRanksInFrontmatter(
newPropName: string,
): Promise<{ updated: number }> {
if (!cohort) return { updated: 0 };
const rankMap = computeRankMap(cohort);
const rankMap = computeRanksForAll(cohort);
return updateCohortFrontmatterProperties(app, files, rankMap, newPropName);
}
@ -268,12 +296,12 @@ export async function writeFrontmatterStatsForPlayer(
cohort: CohortData,
file: TFile,
playerId: string,
precomputedRankMap?: Map<string, number>,
precomputedRankMap: Map<string, number>,
): Promise<void> {
if (!anyEnabled(fm)) return;
const p = cohort.players[playerId];
if (!p) return;
const rankMap = precomputedRankMap ?? computeRankMap(cohort);
const rankMap = precomputedRankMap;
const props = buildProps(fm, {
rating: p.rating,
matches: p.matches,