diff --git a/src/commands/ResetNoteRating.ts b/src/commands/ResetNoteRating.ts index 2349080..c28454e 100644 --- a/src/commands/ResetNoteRating.ts +++ b/src/commands/ResetNoteRating.ts @@ -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); diff --git a/src/domain/report/TemplatePlaceholders.ts b/src/domain/report/TemplatePlaceholders.ts index 43fdf3b..147eb38 100644 --- a/src/domain/report/TemplatePlaceholders.ts +++ b/src/domain/report/TemplatePlaceholders.ts @@ -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); diff --git a/src/main.ts b/src/main.ts index a9b0b01..6439013 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 9009f1f..a22d425 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -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(); 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)); diff --git a/src/storage/PluginDataStore.ts b/src/storage/PluginDataStore.ts index 8e488cc..74c54e7 100644 --- a/src/storage/PluginDataStore.ts +++ b/src/storage/PluginDataStore.ts @@ -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); diff --git a/src/utils/FrontmatterStats.ts b/src/utils/FrontmatterStats.ts index d3985ca..b063d53 100644 --- a/src/utils/FrontmatterStats.ts +++ b/src/utils/FrontmatterStats.ts @@ -17,7 +17,7 @@ function anyEnabled(fm: FrontmatterPropertiesSettings): boolean { } // Standard competition ranking ("1224" style) -export function computeRankMap(cohort: CohortData): Map { +export function computeRanksForAll(cohort: CohortData): Map { 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 { 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 { + 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(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(); + for (const [id, count] of higherCounts) { + rankMap.set(id, count + 1); + } + return rankMap; +} + function buildProps(fm: FrontmatterPropertiesSettings, stats: PlayerStats): Record { const out: Record = {}; @@ -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[] = []; 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, + precomputedRankMap: Map, ): Promise { 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,