diff --git a/src/domain/matchmaking/Matchmaker.ts b/src/domain/matchmaking/Matchmaker.ts index 2bba316..d2efa1c 100644 --- a/src/domain/matchmaking/Matchmaker.ts +++ b/src/domain/matchmaking/Matchmaker.ts @@ -1,10 +1,9 @@ import type { TFile } from 'obsidian'; -import { DEFAULT_SIGMA } from '../rating/GlickoEngine'; import type { ScoredPlayer } from './InfoGain'; import { pickMaxInfoGainPair } from './InfoGain'; -export type RatingStats = { rating: number; sigma?: number }; +export type RatingStats = { rating: number; sigma: number }; export function pickNextPairIndices( files: TFile[], @@ -28,7 +27,7 @@ export function pickNextPairIndices( const players: ScoredPlayer[] = files.map((f, i) => { const s = getStats(f); - return { index: i, rating: s.rating, sigma: s.sigma ?? DEFAULT_SIGMA }; + return { index: i, rating: s.rating, sigma: s.sigma }; }); const result = pickMaxInfoGainPair(players, rng, { lastPairIndices }); diff --git a/src/domain/report/TemplatePlaceholders.ts b/src/domain/report/TemplatePlaceholders.ts index 147eb38..c92f93a 100644 --- a/src/domain/report/TemplatePlaceholders.ts +++ b/src/domain/report/TemplatePlaceholders.ts @@ -184,7 +184,7 @@ export function computePlaceholders( if (cohort) { const allWithSigma = Object.entries(cohort.players) .filter(([, p]) => p.sigma != null) - .map(([id, p]) => ({ id, sigma: p.sigma! })); + .map(([id, p]) => ({ id, sigma: p.sigma })); const sessionWithSigma = allWithSigma.filter(({ id }) => uniquePlayerIds.has(id)); @@ -221,7 +221,7 @@ export function computePlaceholders( } // Leaderboard tables - const buildLeaderboard = (entries: { id: string; rating: number; sigma?: number }[]) => { + const buildLeaderboard = (entries: { id: string; rating: number; sigma: number }[]) => { if (entries.length === 0) return ''; const rows: string[] = []; rows.push('| Rank | Note | Rating |'); @@ -229,7 +229,7 @@ export function computePlaceholders( for (let i = 0; i < entries.length; i++) { const e = entries[i]; const name = idToName(e.id, data.idToPath); - const sigma = e.sigma != null ? ` (σ ${Math.round(e.sigma)})` : ''; + const sigma = ` (σ ${Math.round(e.sigma)})`; rows.push(`| ${i + 1} | ${name} | ${Math.round(e.rating)}${sigma} |`); } return rows.join('\n'); diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index a22d425..070ea0a 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -188,7 +188,7 @@ export default class GlickoSettingsTab extends PluginSettingTab { new Setting(containerEl) .setName('Stability threshold') .setDesc( - `Sigma value at which the progress bar reaches 100%. Lower values require more matches. Default: ${DEFAULT_SETTINGS.stabilityThreshold}.`, + `Uncertainty value at which the progress bar reaches 100%. Lower values require more matches. Default: ${DEFAULT_SETTINGS.stabilityThreshold}.`, ) .addSlider((sl) => { const current = @@ -530,7 +530,7 @@ export default class GlickoSettingsTab extends PluginSettingTab { newProp?: string; }> = []; - const keys: PropKey[] = ['rating', 'rank', 'matches', 'wins']; + const keys: PropKey[] = ['rating', 'uncertainty', 'rank', 'matches', 'wins']; for (const key of keys) { const oldCfg = oldEffective[key]; const newCfg = newEffective[key]; @@ -566,6 +566,8 @@ export default class GlickoSettingsTab extends PluginSettingTab { 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)); + } else if (key === 'uncertainty') { + for (const [id, p] of Object.entries(cohort.players)) map.set(id, Math.round(p.sigma)); } else if (key === 'matches') { for (const [id, p] of Object.entries(cohort.players)) map.set(id, p.matches); } else if (key === 'wins') { diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 7f89383..cd0c9e1 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -8,6 +8,7 @@ export interface FrontmatterPropertyConfig { export interface FrontmatterPropertiesSettings { rating: FrontmatterPropertyConfig; + uncertainty: FrontmatterPropertyConfig; rank: FrontmatterPropertyConfig; matches: FrontmatterPropertyConfig; wins: FrontmatterPropertyConfig; @@ -42,6 +43,7 @@ export const DEFAULT_SETTINGS: GlickoSettings = { sessionLayout: 'new-tab', frontmatterProperties: { rating: { property: 'glickoRating', enabled: false }, + uncertainty: { property: 'glickoUncertainty', enabled: false }, rank: { property: 'glickoRank', enabled: false }, matches: { property: 'glickoMatches', enabled: false }, wins: { property: 'glickoWins', enabled: false }, @@ -68,6 +70,7 @@ export function effectiveFrontmatterProperties( ): FrontmatterPropertiesSettings { return { rating: overrides?.rating ?? base.rating, + uncertainty: overrides?.uncertainty ?? base.uncertainty, rank: overrides?.rank ?? base.rank, matches: overrides?.matches ?? base.matches, wins: overrides?.wins ?? base.wins, diff --git a/src/storage/PluginDataStore.ts b/src/storage/PluginDataStore.ts index 74c54e7..ccd3118 100644 --- a/src/storage/PluginDataStore.ts +++ b/src/storage/PluginDataStore.ts @@ -60,6 +60,12 @@ function mergeSettings(raw?: Partial): GlickoSettings { const out: GlickoSettings = { ...base, ...raw }; + // Deep-merge frontmatterProperties so new keys (e.g. uncertainty) get defaults + out.frontmatterProperties = { + ...DEFAULT_SETTINGS.frontmatterProperties, + ...raw.frontmatterProperties, + }; + if (noIdPropertyName) { out.idPropertyName = 'eloId'; } @@ -183,13 +189,20 @@ export class PluginDataStore { resetPlayer(cohortKey: string, playerId: string): boolean { const cohort = this.store.cohorts[cohortKey]; if (!cohort?.players[playerId]) return false; - cohort.players[playerId] = { rating: 1500, matches: 0, wins: 0 }; + cohort.players[playerId] = { rating: 1500, matches: 0, wins: 0, sigma: DEFAULT_SIGMA }; return true; } ensurePlayer(cohortKey: string, id: string) { const cohort = (this.store.cohorts[cohortKey] ??= { players: {} } as CohortData); - const player = (cohort.players[id] ??= { rating: 1500, matches: 0, wins: 0 }); + const player = (cohort.players[id] ??= { + rating: 1500, + matches: 0, + wins: 0, + sigma: DEFAULT_SIGMA, + }); + // Backfill legacy players that predate sigma tracking + if (player.sigma === undefined) (player as { sigma: number }).sigma = DEFAULT_SIGMA; return { cohort, player }; } @@ -201,8 +214,8 @@ export class PluginDataStore { ): { winnerId?: string; undo: UndoFrame } { const cohort = (this.store.cohorts[cohortKey] ??= { players: {} }); - const a = (cohort.players[aId] ??= { rating: 1500, matches: 0, wins: 0 }); - const b = (cohort.players[bId] ??= { rating: 1500, matches: 0, wins: 0 }); + const a = (cohort.players[aId] ??= { rating: 1500, matches: 0, wins: 0, sigma: DEFAULT_SIGMA }); + const b = (cohort.players[bId] ??= { rating: 1500, matches: 0, wins: 0, sigma: DEFAULT_SIGMA }); const now = Date.now(); @@ -220,8 +233,8 @@ export class PluginDataStore { // 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); + const preSigmaA = inflateSigma(a.sigma, now - (a.lastMatchAt ?? now), cap); + const preSigmaB = inflateSigma(b.sigma, now - (b.lastMatchAt ?? now), cap); const sA = result === 'A' ? 1 : result === 'D' ? 0.5 : 0; const resA = glickoUpdate(a.rating, b.rating, preSigmaA, preSigmaB, sA); @@ -327,7 +340,7 @@ function snapshot( rating: number, matches: number, wins: number, - sigma?: number, + sigma: number, lastMatchAt?: number, ): PlayerSnapshot { return { id, rating, matches, wins, sigma, lastMatchAt }; diff --git a/src/types.ts b/src/types.ts index 4c2c31e..ae68c54 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ export interface Player { rating: number; matches: number; wins: number; - sigma?: number; + sigma: number; lastMatchAt?: number; } @@ -71,7 +71,7 @@ export interface PlayerSnapshot { rating: number; matches: number; wins: number; - sigma?: number; + sigma: number; lastMatchAt?: number; } diff --git a/src/ui/ArenaSession.ts b/src/ui/ArenaSession.ts index d301539..04f118d 100644 --- a/src/ui/ArenaSession.ts +++ b/src/ui/ArenaSession.ts @@ -321,7 +321,7 @@ export default class ArenaSession { let playedSum = 0; for (const p of players) { - playedSum += p.sigma ?? DEFAULT_SIGMA; + playedSum += p.sigma; } // Files not yet in the player table carry full uncertainty @@ -739,7 +739,7 @@ export default class ArenaSession { // ---- Matchmaking helpers ---- - private getStatsForFile(file: TFile): { rating: number; sigma?: number } { + private getStatsForFile(file: TFile): { rating: number; sigma: number } { const id = this.idByPath.get(file.path); const cohort = this.plugin.dataStore.store.cohorts[this.cohortKey]; @@ -748,7 +748,7 @@ export default class ArenaSession { if (p) return { rating: p.rating, sigma: p.sigma }; } - return { rating: 1500 }; + return { rating: 1500, sigma: DEFAULT_SIGMA }; } private pickNextPair() { diff --git a/src/ui/CohortOptionsModal.ts b/src/ui/CohortOptionsModal.ts index 146856e..e0b404b 100644 --- a/src/ui/CohortOptionsModal.ts +++ b/src/ui/CohortOptionsModal.ts @@ -106,6 +106,7 @@ export class CohortOptionsModal extends BasePromiseModal = { label: 'Rating', desc: 'Write the current Glicko rating to this property.', }, + uncertainty: { + label: 'Uncertainty', + desc: 'Write how uncertain the rating is. Starts high and decreases as more comparisons are made.', + }, rank: { label: 'Rank', desc: 'Write the cohort rank (1 = highest) to this property.', diff --git a/src/utils/FrontmatterStats.ts b/src/utils/FrontmatterStats.ts index b063d53..c842b90 100644 --- a/src/utils/FrontmatterStats.ts +++ b/src/utils/FrontmatterStats.ts @@ -7,13 +7,20 @@ import { getNoteId } from './NoteIds'; type PlayerStats = { rating: number; + uncertainty: number; matches: number; wins: number; rank: number; }; function anyEnabled(fm: FrontmatterPropertiesSettings): boolean { - return !!fm.rating.enabled || !!fm.rank.enabled || !!fm.matches.enabled || !!fm.wins.enabled; + return ( + !!fm.rating.enabled || + !!fm.uncertainty.enabled || + !!fm.rank.enabled || + !!fm.matches.enabled || + !!fm.wins.enabled + ); } // Standard competition ranking ("1224" style) @@ -71,6 +78,9 @@ function buildProps(fm: FrontmatterPropertiesSettings, stats: PlayerStats): Reco if (fm.rating.enabled && fm.rating.property) { out[fm.rating.property] = Math.round(stats.rating); } + if (fm.uncertainty.enabled && fm.uncertainty.property) { + out[fm.uncertainty.property] = Math.round(stats.uncertainty); + } if (fm.rank.enabled && fm.rank.property) { out[fm.rank.property] = stats.rank; } @@ -304,6 +314,7 @@ export async function writeFrontmatterStatsForPlayer( const rankMap = precomputedRankMap; const props = buildProps(fm, { rating: p.rating, + uncertainty: p.sigma, matches: p.matches, wins: p.wins, rank: rankMap.get(playerId) ?? rankMap.size,