AI Implementation feature(911): Scoreboard: Ranking, Matrix, Event Log and Score Graph 1.01 (#54)

This commit was merged in pull request #54.
This commit is contained in:
2026-07-23 06:03:13 +00:00
parent 4273662211
commit a39a7daee8
7 changed files with 365 additions and 56 deletions
@@ -94,6 +94,32 @@ export function stablePlayerColorIndex(playerId: string): number {
return ((hash % n) + n) % n;
}
export function assignUniquePlayerColors<T extends { playerId: string }>(
items: readonly T[],
getColorIndex: (t: T) => number,
capacity: number = PLAYER_COLOR_PALETTE.length,
): T[] {
const used = new Set<number>();
const cap = Math.max(1, Math.floor(capacity));
const out: T[] = new Array(items.length);
for (let i = 0; i < items.length; i += 1) {
const item = items[i];
const preferredRaw = getColorIndex(item);
const preferred = ((preferredRaw % cap) + cap) % cap;
let chosen = preferred;
if (used.has(preferred)) {
let probe = (preferred + 1) % cap;
while (used.has(probe) && probe !== preferred) {
probe = (probe + 1) % cap;
}
chosen = probe;
}
used.add(chosen);
out[i] = { ...item, colorIndex: chosen } as T;
}
return out;
}
export function applyRankingSort(rows: readonly RankingRow[]): RankingRow[] {
const list = rows.map((r) => ({ ...r }));
list.sort((a, b) => {
@@ -136,7 +162,8 @@ export function parseSolveEventIntoRanking(
colorIndex: stablePlayerColorIndex(payload.playerId),
});
}
return applyRankingSort(out);
const sorted = applyRankingSort(out);
return assignUniquePlayerColors(sorted, (r) => r.colorIndex);
}
export function dedupEventLogBySolveId(
@@ -221,8 +248,9 @@ export function applySolveToGraph(
const finalValue = (s: GraphSeries) => s.points[s.points.length - 1]?.value ?? 0;
seriesList.sort((a, b) => finalValue(b) - finalValue(a));
const top = seriesList.slice(0, 10);
const uniqueTop = assignUniquePlayerColors(top, (s) => s.colorIndex);
return { startUtc: start, endUtc: end, serverNowUtc, state, series: top };
return { startUtc: start, endUtc: end, serverNowUtc, state, series: uniqueTop };
}
export function mutateMatrixFromSolve(
@@ -251,7 +279,8 @@ export function mutateMatrixFromSolve(
cellRow[payload.challengeId] = 'solved';
}
cells[payload.playerId] = cellRow;
return { players, challenges: matrix.challenges, cells };
const uniquePlayers = assignUniquePlayerColors(players, (p) => p.colorIndex);
return { players: uniquePlayers, challenges: matrix.challenges, cells };
}
export function formatSolveDateTime(utc: string | null | undefined): string {
@@ -9,6 +9,7 @@ import {
ScoreboardTab,
SolveLivePayload,
applySolveToGraph,
assignUniquePlayerColors,
dedupEventLogBySolveId,
mutateMatrixFromSolve,
parseSolveEventIntoRanking,
@@ -67,10 +68,13 @@ export class ScoreboardStore {
const matrix = await this.toPromise<MatrixView>(() => this.api.getMatrix());
const eventLog = await this.toPromise<EventLogRow[]>(() => this.api.getEventLog(50));
const graph = await this.toPromise<GraphView>(() => this.api.getGraph());
this._ranking.set(ranking);
this._matrix.set(matrix);
const uniqueRanking = assignUniquePlayerColors(ranking, (r) => r.colorIndex);
const uniqueMatrixPlayers = assignUniquePlayerColors(matrix.players, (p) => p.colorIndex);
const uniqueGraphSeries = assignUniquePlayerColors(graph.series, (s) => s.colorIndex);
this._ranking.set(uniqueRanking);
this._matrix.set({ ...matrix, players: uniqueMatrixPlayers });
this._eventLog.set(eventLog);
this._graph.set(graph);
this._graph.set({ ...graph, series: uniqueGraphSeries });
} catch (err: any) {
this._error.set(err?.message ?? 'Failed to load scoreboard');
} finally {