+
#{standing.rank}
@@ -68,7 +69,7 @@ export function DistrictEventOverlay({ event, standings }: DistrictEventOverlayP
{standing.formattedScore} {standing.multiplier > 1 ? `${standing.multiplier}×` : ""}
-
+
))}
diff --git a/data/district-leaderboard-snapshots.json b/data/district-leaderboard-snapshots.json
new file mode 100644
index 00000000..3ecab36a
--- /dev/null
+++ b/data/district-leaderboard-snapshots.json
@@ -0,0 +1,26 @@
+[
+ {
+ "weekIndex": 24,
+ "districtId": "data-center",
+ "districtName": "Data Center",
+ "endedAt": "2026-06-28T00:00:00.000Z",
+ "challengeName": "Skill Mastery",
+ "entries": [
+ { "rank": 1, "agentId": "bot-0", "agentName": "Nexus-7", "score": 4, "formattedScore": "4.00", "tasksCompletedThisWeek": 43 },
+ { "rank": 2, "agentId": "bot-10", "agentName": "Vertex-11", "score": 2.67, "formattedScore": "2.67", "tasksCompletedThisWeek": 31 },
+ { "rank": 3, "agentId": "bot-5", "agentName": "Stratos-2", "score": 2.5, "formattedScore": "2.50", "tasksCompletedThisWeek": 24 }
+ ]
+ },
+ {
+ "weekIndex": 24,
+ "districtId": "comm-hub",
+ "districtName": "Comm Hub",
+ "endedAt": "2026-06-28T00:00:00.000Z",
+ "challengeName": "Skill Mastery",
+ "entries": [
+ { "rank": 1, "agentId": "bot-1", "agentName": "Cipher-3", "score": 3.5, "formattedScore": "3.50", "tasksCompletedThisWeek": 38 },
+ { "rank": 2, "agentId": "bot-6", "agentName": "Bolt-8", "score": 2.25, "formattedScore": "2.25", "tasksCompletedThisWeek": 27 },
+ { "rank": 3, "agentId": "bot-11", "agentName": "Echo-12", "score": 2, "formattedScore": "2.00", "tasksCompletedThisWeek": 20 }
+ ]
+ }
+]
diff --git a/lib/gamification/district-leaderboard.ts b/lib/gamification/district-leaderboard.ts
new file mode 100644
index 00000000..35bbb39d
--- /dev/null
+++ b/lib/gamification/district-leaderboard.ts
@@ -0,0 +1,80 @@
+import { existsSync, readFileSync } from "node:fs"
+import path from "node:path"
+import { DISTRICTS } from "@/lib/data"
+import { formatDistrictScore, getActiveDistrictEvent, getAgentDistrictCompetitionScore, getWeekBounds, getWeeklyTasksCompleted } from "@/lib/gamification/events"
+import type { DistrictId, MoltbotAgent } from "@/lib/types"
+
+export interface DistrictLeaderboardEntry {
+ rank: number
+ agentId: string
+ agentName: string
+ score: number
+ formattedScore: string
+ tasksCompletedThisWeek: number
+ districtId: DistrictId
+ districtName: string
+}
+
+export interface DistrictLeaderboardSnapshot {
+ weekIndex: number
+ districtId: DistrictId
+ districtName: string
+ endedAt: string
+ challengeName: string
+ entries: Omit
[]
+}
+
+const SNAPSHOT_PATH = path.join(process.cwd(), "data", "district-leaderboard-snapshots.json")
+
+export function getDistrictLeaderboard(agents: MoltbotAgent[], districtId: DistrictId, now: Date = new Date()): DistrictLeaderboardEntry[] {
+ const event = getActiveDistrictEvent(now)
+ const district = DISTRICTS.find((entry) => entry.id === districtId)
+ const districtName = district?.name ?? districtId
+
+ return agents
+ .filter((agent) => agent.district === districtId)
+ .map((agent) => {
+ const score = getAgentDistrictCompetitionScore(agent, event.challenge, event.weekIndex)
+ return {
+ rank: 0,
+ agentId: agent.id,
+ agentName: agent.name,
+ score: Number(score.toFixed(3)),
+ formattedScore: formatDistrictScore(event.challenge, score),
+ tasksCompletedThisWeek: getWeeklyTasksCompleted(agent, event.weekIndex),
+ districtId,
+ districtName,
+ }
+ })
+ .sort((a, b) => event.challenge.better === "lower" ? a.score - b.score : b.score - a.score)
+ .slice(0, 10)
+ .map((entry, index) => ({ ...entry, rank: index + 1 }))
+}
+
+function isSnapshot(value: unknown): value is DistrictLeaderboardSnapshot {
+ if (!value || typeof value !== "object") return false
+ const candidate = value as Partial
+ return typeof candidate.weekIndex === "number"
+ && typeof candidate.districtId === "string"
+ && typeof candidate.districtName === "string"
+ && typeof candidate.endedAt === "string"
+ && typeof candidate.challengeName === "string"
+ && Array.isArray(candidate.entries)
+}
+
+export function readDistrictLeaderboardSnapshots(): DistrictLeaderboardSnapshot[] {
+ if (!existsSync(SNAPSHOT_PATH)) return []
+ try {
+ const parsed: unknown = JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8"))
+ return Array.isArray(parsed) ? parsed.filter(isSnapshot) : []
+ } catch {
+ return []
+ }
+}
+
+export function getPreviousDistrictLeaderboardSnapshot(districtId: DistrictId, now: Date = new Date()): DistrictLeaderboardSnapshot | null {
+ const previousWeekIndex = Math.max(0, getWeekBounds(now).weekIndex - 1)
+ return readDistrictLeaderboardSnapshots()
+ .filter((snapshot) => snapshot.districtId === districtId && snapshot.weekIndex <= previousWeekIndex)
+ .sort((a, b) => b.weekIndex - a.weekIndex)[0] ?? null
+}
diff --git a/lib/gamification/events.ts b/lib/gamification/events.ts
index bf750505..7272893e 100644
--- a/lib/gamification/events.ts
+++ b/lib/gamification/events.ts
@@ -66,9 +66,9 @@ export const DISTRICT_CHALLENGES: DistrictChallengeDefinition[] = [
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
const DAY_MS = 24 * 60 * 60 * 1000
-const EPOCH_START_MS = Date.UTC(2026, 0, 5, 0, 0, 0, 0)
+const EPOCH_START_MS = Date.UTC(2026, 0, 4, 0, 0, 0, 0)
-function getWeekBounds(now: Date = new Date()): { weekIndex: number; startedAt: Date; endsAt: Date } {
+export function getWeekBounds(now: Date = new Date()): { weekIndex: number; startedAt: Date; endsAt: Date } {
const elapsed = Math.max(0, now.getTime() - EPOCH_START_MS)
const weekIndex = Math.floor(elapsed / WEEK_MS)
const startedAt = new Date(EPOCH_START_MS + weekIndex * WEEK_MS)
@@ -84,7 +84,7 @@ function seededNoise(seed: string): number {
return (hash >>> 0) / 4294967295
}
-function formatScore(challenge: DistrictChallengeDefinition, score: number): string {
+export function formatDistrictScore(challenge: DistrictChallengeDefinition, score: number): string {
if (challenge.type === "revenue") return `$${score.toFixed(2)}`
if (challenge.type === "uptime") return `${score.toFixed(1)}%`
if (challenge.type === "speed") return `${score.toFixed(1)}m`
@@ -92,7 +92,7 @@ function formatScore(challenge: DistrictChallengeDefinition, score: number): str
return String(Math.round(score))
}
-function getAgentScore(agent: MoltbotAgent, challenge: DistrictChallengeDefinition, weekIndex: number): number {
+export function getAgentDistrictCompetitionScore(agent: MoltbotAgent, challenge: DistrictChallengeDefinition, weekIndex: number): number {
const noise = seededNoise(`${weekIndex}:${challenge.type}:${agent.id}`)
switch (challenge.type) {
case "throughput":
@@ -108,6 +108,11 @@ function getAgentScore(agent: MoltbotAgent, challenge: DistrictChallengeDefiniti
}
}
+export function getWeeklyTasksCompleted(agent: MoltbotAgent, weekIndex: number): number {
+ const completionRatio = 0.12 + seededNoise(`${weekIndex}:weekly-tasks:${agent.id}`) * 0.28
+ return Math.max(0, Math.round(agent.tasksCompleted * completionRatio))
+}
+
export function getActiveDistrictEvent(now: Date = new Date()): ActiveDistrictEvent {
const bounds = getWeekBounds(now)
const challenge = DISTRICT_CHALLENGES[bounds.weekIndex % DISTRICT_CHALLENGES.length]
@@ -129,7 +134,7 @@ export function getDistrictStandings(agents: MoltbotAgent[], now: Date = new Dat
const event = getActiveDistrictEvent(now)
const standings = DISTRICTS.map((district) => {
const districtAgents = agents.filter((agent) => agent.district === district.id)
- const scoredAgents = districtAgents.map((agent) => ({ agent, score: getAgentScore(agent, event.challenge, event.weekIndex) }))
+ const scoredAgents = districtAgents.map((agent) => ({ agent, score: getAgentDistrictCompetitionScore(agent, event.challenge, event.weekIndex) }))
const score = event.challenge.type === "speed"
? (scoredAgents.reduce((sum, entry) => sum + entry.score, 0) / Math.max(1, scoredAgents.length))
: scoredAgents.reduce((sum, entry) => sum + entry.score, 0) / (event.challenge.type === "uptime" || event.challenge.type === "skill" ? Math.max(1, scoredAgents.length) : 1)
@@ -143,14 +148,14 @@ export function getDistrictStandings(agents: MoltbotAgent[], now: Date = new Dat
districtName: entry.district.name,
color: entry.district.color,
score: Number(entry.score.toFixed(3)),
- formattedScore: formatScore(event.challenge, entry.score),
+ formattedScore: formatDistrictScore(event.challenge, entry.score),
rank: index + 1,
multiplier: index === 0 ? 2 : index === 1 ? 1.5 : 1,
topAgent: entry.top ? {
id: entry.top.agent.id,
name: entry.top.agent.name,
score: Number(entry.top.score.toFixed(3)),
- formattedScore: formatScore(event.challenge, entry.top.score),
+ formattedScore: formatDistrictScore(event.challenge, entry.top.score),
} : null,
}))
}