1// Two leaderboards over a game's analysis: by model (every agent that2// reported that model, aggregated: is one model winning overall) and by3// user (machine key or join name, aggregated across whatever models they4// ran, listing those models: a person farming the board with unusual5// model names is visible). Daily boards come from one state; cumulative6// boards merge days. Pure functions over the fold's output.7import type { AgentInfo, State } from "./fold.ts";89export type Row = {10 key: string; // the model, or the user11 name?: string; // by user: the display name when the key is a machine id12 verified?: boolean; // by user: a machine key stands behind this row13 reward: number; // cells solved14 tiles: number;15 forced: number;16 guess: number;17 wrong: number;18 wrong_rate: number; // wrong / (forced + guess + wrong)19 dead: number; // agents in this row that died (their reward stands)20 meets: number;21 agents: number; // agent joins aggregated into this row22 models: string[]; // by user: the models this user ran23 users: string[]; // by model: the users who ran it24 days: number; // cumulative: days with at least one join25};2627export type Boards = {28 day: string | null;29 game_id: string;30 by_model: Row[];31 by_user: Row[];32};3334/** The user behind an agent: its verified machine id; else (provisional35 * agents) the name it gave, which is only a label. */36export function userOf(a: AgentInfo): string {37 if (a.verified && a.machine_id) return a.machine_id;38 if (!/^a\d+$/.test(a.id)) return a.id;39 return a.name && a.name !== a.id ? a.name : a.id;40}4142export function modelOf(a: AgentInfo): string {43 return a.model ?? "unreported";44}4546function emptyRow(key: string): Row {47 return {48 key,49 reward: 0,50 tiles: 0,51 forced: 0,52 guess: 0,53 wrong: 0,54 wrong_rate: 0,55 dead: 0,56 meets: 0,57 agents: 0,58 models: [],59 users: [],60 days: 0,61 };62}6364function rateOf(r: Row): number {65 const n = r.forced + r.guess + r.wrong;66 return n ? r.wrong / n : 0;67}6869function addTo(list: string[], v: string): void {70 if (!list.includes(v)) list.push(v);71}7273function sortRows(rows: Map<string, Row>): Row[] {74 return [...rows.values()].sort((a, b) =>75 b.reward - a.reward || b.tiles - a.tiles || a.guess - b.guess ||76 a.key.localeCompare(b.key)77 );78}7980/** The two boards of one game at its current state. */81export function boardsOf(s: State): Boards {82 const byModel = new Map<string, Row>();83 const byUser = new Map<string, Row>();84 for (const a of s.agents) {85 const an = s.analysis.agents.find((x) => x.agent === a.id);86 const model = modelOf(a);87 const user = userOf(a);88 for (89 const [map, key, other, list] of [90 [byModel, model, user, "users"],91 [byUser, user, model, "models"],92 ] as const93 ) {94 const row = map.get(key) ?? emptyRow(key);95 row.agents++;96 addTo(row[list], other);97 // by user: a machine id keys the row; the agent's name is what to show98 if (list === "models" && a.name && a.name !== key && !row.name) {99 row.name = a.name;100 }101 if (list === "models") {102 row.verified = (row.verified ?? false) || a.verified;103 }104 if (an) {105 row.reward += an.reward;106 row.tiles += an.tiles;107 row.forced += an.forced;108 row.guess += an.guess;109 row.wrong += an.wrong;110 row.meets += an.meets;111 if (an.health.state === "dead") row.dead++;112 }113 row.wrong_rate = rateOf(row);114 row.days = 1;115 map.set(key, row);116 }117 }118 return {119 day: s.day,120 game_id: s.game_id,121 by_model: sortRows(byModel),122 by_user: sortRows(byUser),123 };124}125126/** Cumulative boards over several days' boards. */127export function mergeBoards(128 days: Boards[],129): { by_model: Row[]; by_user: Row[]; days: number } {130 const merge = (pick: (b: Boards) => Row[]) => {131 const out = new Map<string, Row>();132 for (const b of days) {133 for (const r of pick(b)) {134 const row = out.get(r.key) ?? emptyRow(r.key);135 row.reward += r.reward;136 row.tiles += r.tiles;137 row.forced += r.forced;138 row.guess += r.guess;139 row.wrong += r.wrong;140 row.dead += r.dead ?? 0;141 row.meets += r.meets;142 row.agents += r.agents;143 row.wrong_rate = rateOf(row);144 row.days += 1;145 if (r.verified) row.verified = true;146 for (const m of r.models) addTo(row.models, m);147 for (const u of r.users) addTo(row.users, u);148 out.set(r.key, row);149 }150 }151 return sortRows(out);152 };153 return {154 by_model: merge((b) => b.by_model),155 by_user: merge((b) => b.by_user),156 days: days.length,157 };158}159160/** The winners of a day: the top VERIFIED user (crowned tomorrow: only a161 * machine with a key can be honoured) and the top model (named on162 * tomorrow's start card). Null when nobody scored. */163export function winnersOf(164 b: Boards,165): { user: Row | null; model: Row | null } {166 const user = b.by_user.find((r) => r.reward > 0 && r.verified) ?? null;167 const model = b.by_model.find((r) => r.reward > 0) ?? null;168 return { user, model };169}