mirror of
https://github.com/williamsjack/pairwise-glicko-ranking.git
synced 2026-07-22 06:44:09 +00:00
Expose rating uncertainty in frontmatter
This commit is contained in:
parent
1f7abe4a7d
commit
42bbe6b289
10 changed files with 62 additions and 23 deletions
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ function mergeSettings(raw?: Partial<GlickoSettings>): 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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export class CohortOptionsModal extends BasePromiseModal<CohortOptionsResult | u
|
|||
|
||||
this.working = {
|
||||
rating: mk('rating'),
|
||||
uncertainty: mk('uncertainty'),
|
||||
rank: mk('rank'),
|
||||
matches: mk('matches'),
|
||||
wins: mk('wins'),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import type { TextComponent, ToggleComponent } from 'obsidian';
|
||||
import { Setting } from 'obsidian';
|
||||
|
||||
export type FmPropKey = 'rating' | 'rank' | 'matches' | 'wins';
|
||||
export const FM_PROP_KEYS: readonly FmPropKey[] = ['rating', 'rank', 'matches', 'wins'] as const;
|
||||
export type FmPropKey = 'rating' | 'uncertainty' | 'rank' | 'matches' | 'wins';
|
||||
export const FM_PROP_KEYS: readonly FmPropKey[] = [
|
||||
'rating',
|
||||
'uncertainty',
|
||||
'rank',
|
||||
'matches',
|
||||
'wins',
|
||||
] as const;
|
||||
|
||||
type Meta = { label: string; desc: string };
|
||||
|
||||
|
|
@ -11,6 +17,10 @@ const META: Record<FmPropKey, Meta> = {
|
|||
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.',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue