@flare-engine/leaderboards 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -54,12 +54,6 @@ interface MemoryLeaderboardsOptions {
54
54
  /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */
55
55
  seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;
56
56
  }
57
- /**
58
- * The in-memory provider IS a {@link LeaderboardsProvider} — its backend-free
59
- * nature is an implementation detail, not a wider surface. See
60
- * {@link createMemoryLeaderboards} for the recorded semantics.
61
- */
62
- type MemoryLeaderboards = LeaderboardsProvider;
63
57
  /**
64
58
  * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for
65
59
  * local development, tests, and single-player games.
@@ -81,7 +75,7 @@ type MemoryLeaderboards = LeaderboardsProvider;
81
75
  * - `getPlayerRank(id)` returns the local player's 1-based rank under the same
82
76
  * ordering, or `null` when the local player has no entry on that board.
83
77
  */
84
- declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): MemoryLeaderboards;
78
+ declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider;
85
79
  //#endregion
86
- export { type LeaderboardId, LeaderboardsContext, type LeaderboardsProvider, type MemoryLeaderboards, type MemoryLeaderboardsOptions, NoopLeaderboardsProvider, type ScoreEntry, type SeedScore, createMemoryLeaderboards, useLeaderboards };
80
+ export { type LeaderboardId, LeaderboardsContext, type LeaderboardsProvider, type MemoryLeaderboardsOptions, NoopLeaderboardsProvider, type ScoreEntry, type SeedScore, createMemoryLeaderboards, useLeaderboards };
87
81
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":";;KACY;;UAGK;EACf;EACA;EACA;EACA;EACA,OAAO;;;;;;;;UASQ;EACf,YAAY,IAAI,eAAe,eAAe,OAAO,0BAA0B;EAC/E,QAAQ,IAAI,eAAe,YAAY,iBAAiB;EACxD,cAAc,IAAI,gBAAgB;;;;;;cAOvB,0BAA0B;;;;;;;;;cCnB1B,qCAAmB,QAAA;;iBAGhB,mBAAmB;;;;UCTlB;EACf;EACA;EACA;EACA,OAAO;;;UAIQ;;EAEf;;EAEA;;EAEA,OAAO,SAAS,OAAO,wBAAwB;;;;;;;KAQrC,qBAAqB;;;;;;;;;;;;;;;;;;;;;;iBA8CjB,yBAAyB,OAAO,4BAA4B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":";;KACY;;UAGK;EACf;EACA;EACA;EACA;EACA,OAAO;;;;;;;;UASQ;EACf,YAAY,IAAI,eAAe,eAAe,OAAO,0BAA0B;EAC/E,QAAQ,IAAI,eAAe,YAAY,iBAAiB;EACxD,cAAc,IAAI,gBAAgB;;;;;;cAOvB,0BAA0B;;;;;;;;;cCnB1B,qCAAmB,QAAA;;iBAGhB,mBAAmB;;;;UCTlB;EACf;EACA;EACA;EACA,OAAO;;;UAIQ;;EAEf;;EAEA;;EAEA,OAAO,SAAS,OAAO,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;iBA+CjC,yBAAyB,OAAO,4BAA4B"}
package/dist/index.d.ts CHANGED
@@ -54,12 +54,6 @@ interface MemoryLeaderboardsOptions {
54
54
  /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */
55
55
  seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;
56
56
  }
57
- /**
58
- * The in-memory provider IS a {@link LeaderboardsProvider} — its backend-free
59
- * nature is an implementation detail, not a wider surface. See
60
- * {@link createMemoryLeaderboards} for the recorded semantics.
61
- */
62
- type MemoryLeaderboards = LeaderboardsProvider;
63
57
  /**
64
58
  * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for
65
59
  * local development, tests, and single-player games.
@@ -81,7 +75,7 @@ type MemoryLeaderboards = LeaderboardsProvider;
81
75
  * - `getPlayerRank(id)` returns the local player's 1-based rank under the same
82
76
  * ordering, or `null` when the local player has no entry on that board.
83
77
  */
84
- declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): MemoryLeaderboards;
78
+ declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider;
85
79
  //#endregion
86
- export { type LeaderboardId, LeaderboardsContext, type LeaderboardsProvider, type MemoryLeaderboards, type MemoryLeaderboardsOptions, NoopLeaderboardsProvider, type ScoreEntry, type SeedScore, createMemoryLeaderboards, useLeaderboards };
80
+ export { type LeaderboardId, LeaderboardsContext, type LeaderboardsProvider, type MemoryLeaderboardsOptions, NoopLeaderboardsProvider, type ScoreEntry, type SeedScore, createMemoryLeaderboards, useLeaderboards };
87
81
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":";;KACY;;UAGK;EACf;EACA;EACA;EACA;EACA,OAAO;;;;;;;;UASQ;EACf,YAAY,IAAI,eAAe,eAAe,OAAO,0BAA0B;EAC/E,QAAQ,IAAI,eAAe,YAAY,iBAAiB;EACxD,cAAc,IAAI,gBAAgB;;;;;;cAOvB,0BAA0B;;;;;;;;;cCnB1B,qCAAmB,QAAA;;iBAGhB,mBAAmB;;;;UCTlB;EACf;EACA;EACA;EACA,OAAO;;;UAIQ;;EAEf;;EAEA;;EAEA,OAAO,SAAS,OAAO,wBAAwB;;;;;;;KAQrC,qBAAqB;;;;;;;;;;;;;;;;;;;;;;iBA8CjB,yBAAyB,OAAO,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":";;KACY;;UAGK;EACf;EACA;EACA;EACA;EACA,OAAO;;;;;;;;UASQ;EACf,YAAY,IAAI,eAAe,eAAe,OAAO,0BAA0B;EAC/E,QAAQ,IAAI,eAAe,YAAY,iBAAiB;EACxD,cAAc,IAAI,gBAAgB;;;;;;cAOvB,0BAA0B;;;;;;;;;cCnB1B,qCAAmB,QAAA;;iBAGhB,mBAAmB;;;;UCTlB;EACf;EACA;EACA;EACA,OAAO;;;UAIQ;;EAEf;;EAEA;;EAEA,OAAO,SAAS,OAAO,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;iBA+CjC,yBAAyB,OAAO,4BAA4B"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\n/**\n * The in-memory provider IS a {@link LeaderboardsProvider} — its backend-free\n * nature is an implementation detail, not a wider surface. See\n * {@link createMemoryLeaderboards} for the recorded semantics.\n */\nexport type MemoryLeaderboards = LeaderboardsProvider;\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): MemoryLeaderboards {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF;;;;;;;;;AC7BA,MAAa,uBAAA,GAAA,MAAA,cAAA,CAA0D,wBAAwB;;AAG/F,SAAgB,kBAAwC;CACtD,QAAA,GAAA,MAAA,WAAA,CAAkB,mBAAmB;AACvC;;;;ACuBA,SAAS,OAAO,SAAiD;CAC/D,OAAO,QACJ,MAAM,CAAC,CACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACtD,KAAK,GAAG,OAAO;EACd,UAAU,EAAE;EACZ,aAAa,EAAE;EACf,OAAO,EAAE;EACT,MAAM,IAAI;EACV,MAAM,EAAE;CACV,EAAE;AACN;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,yBAAyB,MAAsD;CAC7F,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,yBAAS,IAAI,IAAoC;CACvD,IAAI,YAAY;CAEhB,IAAI,MAAM,SAAS,KAAA,GACjB,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,QAAyB,KAAK,KAAK,SAAS;GAChD,UAAU,IAAI;GACd,aAAa,IAAI;GACjB,OAAO,IAAI;GACX,MAAM,IAAI;GACV,OAAO;EACT,EAAE;EACF,OAAO,IAAI,IAAI,KAAK;CACtB;CAGF,OAAO;EACL,MAAM,YAAY,IAAI,OAAO,MAAqB;GAChD,IAAI,QAAQ,OAAO,IAAI,EAAE;GACzB,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,CAAC;IACT,OAAO,IAAI,IAAI,KAAK;GACtB;GACA,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO;GACzD,IAAI,aAAa,KAAA,GACf,MAAM,KAAK;IACT,UAAU;IACV,aAAa;IACb;IACA;IACA,OAAO;GACT,CAAC;QACI,IAAI,QAAQ,SAAS,OAAO;IACjC,SAAS,QAAQ;IACjB,SAAS,OAAO;GAClB;EACF;EAEA,MAAM,QAAQ,IAAI,GAAmC;GACnD,IAAI,KAAK,GACP,OAAO,CAAC;GAEV,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO,CAAC;GAEV,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;EACjC;EAEA,MAAM,cAAc,IAA4B;GAC9C,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO;GAET,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,EAAE,aAAa,OAAO;GAC9D,OAAO,UAAU,KAAA,IAAY,OAAO,MAAM;EAC5C;CACF;AACF"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF;;;;;;;;;AC7BA,MAAa,uBAAA,GAAA,MAAA,cAAA,CAA0D,wBAAwB;;AAG/F,SAAgB,kBAAwC;CACtD,QAAA,GAAA,MAAA,WAAA,CAAkB,mBAAmB;AACvC;;;;ACgBA,SAAS,OAAO,SAAiD;CAC/D,OAAO,QACJ,MAAM,CAAC,CACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACtD,KAAK,GAAG,OAAO;EACd,UAAU,EAAE;EACZ,aAAa,EAAE;EACf,OAAO,EAAE;EACT,MAAM,IAAI;EACV,MAAM,EAAE;CACV,EAAE;AACN;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,yBAAyB,MAAwD;CAC/F,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,yBAAS,IAAI,IAAoC;CACvD,IAAI,YAAY;CAEhB,IAAI,MAAM,SAAS,KAAA,GACjB,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,QAAyB,KAAK,KAAK,SAAS;GAChD,UAAU,IAAI;GACd,aAAa,IAAI;GACjB,OAAO,IAAI;GACX,MAAM,IAAI;GACV,OAAO;EACT,EAAE;EACF,OAAO,IAAI,IAAI,KAAK;CACtB;CAGF,OAAO;EACL,MAAM,YAAY,IAAI,OAAO,MAAqB;GAChD,IAAI,QAAQ,OAAO,IAAI,EAAE;GACzB,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,CAAC;IACT,OAAO,IAAI,IAAI,KAAK;GACtB;GACA,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO;GACzD,IAAI,aAAa,KAAA,GACf,MAAM,KAAK;IACT,UAAU;IACV,aAAa;IACb;IACA;IACA,OAAO;GACT,CAAC;QACI,IAAI,QAAQ,SAAS,OAAO;IACjC,SAAS,QAAQ;IACjB,SAAS,OAAO;GAClB;EACF;EAEA,MAAM,QAAQ,IAAI,GAAmC;GACnD,IAAI,KAAK,GACP,OAAO,CAAC;GAEV,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO,CAAC;GAEV,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;EACjC;EAEA,MAAM,cAAc,IAA4B;GAC9C,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO;GAET,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,EAAE,aAAa,OAAO;GAC9D,OAAO,UAAU,KAAA,IAAY,OAAO,MAAM;EAC5C;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\n/**\n * The in-memory provider IS a {@link LeaderboardsProvider} — its backend-free\n * nature is an implementation detail, not a wider surface. See\n * {@link createMemoryLeaderboards} for the recorded semantics.\n */\nexport type MemoryLeaderboards = LeaderboardsProvider;\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): MemoryLeaderboards {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF;;;;;;;;;AC7BA,MAAa,sBAAsB,cAAoC,wBAAwB;;AAG/F,SAAgB,kBAAwC;CACtD,OAAO,WAAW,mBAAmB;AACvC;;;;ACuBA,SAAS,OAAO,SAAiD;CAC/D,OAAO,QACJ,MAAM,CAAC,CACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACtD,KAAK,GAAG,OAAO;EACd,UAAU,EAAE;EACZ,aAAa,EAAE;EACf,OAAO,EAAE;EACT,MAAM,IAAI;EACV,MAAM,EAAE;CACV,EAAE;AACN;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,yBAAyB,MAAsD;CAC7F,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,yBAAS,IAAI,IAAoC;CACvD,IAAI,YAAY;CAEhB,IAAI,MAAM,SAAS,KAAA,GACjB,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,QAAyB,KAAK,KAAK,SAAS;GAChD,UAAU,IAAI;GACd,aAAa,IAAI;GACjB,OAAO,IAAI;GACX,MAAM,IAAI;GACV,OAAO;EACT,EAAE;EACF,OAAO,IAAI,IAAI,KAAK;CACtB;CAGF,OAAO;EACL,MAAM,YAAY,IAAI,OAAO,MAAqB;GAChD,IAAI,QAAQ,OAAO,IAAI,EAAE;GACzB,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,CAAC;IACT,OAAO,IAAI,IAAI,KAAK;GACtB;GACA,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO;GACzD,IAAI,aAAa,KAAA,GACf,MAAM,KAAK;IACT,UAAU;IACV,aAAa;IACb;IACA;IACA,OAAO;GACT,CAAC;QACI,IAAI,QAAQ,SAAS,OAAO;IACjC,SAAS,QAAQ;IACjB,SAAS,OAAO;GAClB;EACF;EAEA,MAAM,QAAQ,IAAI,GAAmC;GACnD,IAAI,KAAK,GACP,OAAO,CAAC;GAEV,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO,CAAC;GAEV,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;EACjC;EAEA,MAAM,cAAc,IAA4B;GAC9C,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO;GAET,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,EAAE,aAAa,OAAO;GAC9D,OAAO,UAAU,KAAA,IAAY,OAAO,MAAM;EAC5C;CACF;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF;;;;;;;;;AC7BA,MAAa,sBAAsB,cAAoC,wBAAwB;;AAG/F,SAAgB,kBAAwC;CACtD,OAAO,WAAW,mBAAmB;AACvC;;;;ACgBA,SAAS,OAAO,SAAiD;CAC/D,OAAO,QACJ,MAAM,CAAC,CACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACtD,KAAK,GAAG,OAAO;EACd,UAAU,EAAE;EACZ,aAAa,EAAE;EACf,OAAO,EAAE;EACT,MAAM,IAAI;EACV,MAAM,EAAE;CACV,EAAE;AACN;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,yBAAyB,MAAwD;CAC/F,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,yBAAS,IAAI,IAAoC;CACvD,IAAI,YAAY;CAEhB,IAAI,MAAM,SAAS,KAAA,GACjB,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,QAAyB,KAAK,KAAK,SAAS;GAChD,UAAU,IAAI;GACd,aAAa,IAAI;GACjB,OAAO,IAAI;GACX,MAAM,IAAI;GACV,OAAO;EACT,EAAE;EACF,OAAO,IAAI,IAAI,KAAK;CACtB;CAGF,OAAO;EACL,MAAM,YAAY,IAAI,OAAO,MAAqB;GAChD,IAAI,QAAQ,OAAO,IAAI,EAAE;GACzB,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,CAAC;IACT,OAAO,IAAI,IAAI,KAAK;GACtB;GACA,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO;GACzD,IAAI,aAAa,KAAA,GACf,MAAM,KAAK;IACT,UAAU;IACV,aAAa;IACb;IACA;IACA,OAAO;GACT,CAAC;QACI,IAAI,QAAQ,SAAS,OAAO;IACjC,SAAS,QAAQ;IACjB,SAAS,OAAO;GAClB;EACF;EAEA,MAAM,QAAQ,IAAI,GAAmC;GACnD,IAAI,KAAK,GACP,OAAO,CAAC;GAEV,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO,CAAC;GAEV,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;EACjC;EAEA,MAAM,cAAc,IAA4B;GAC9C,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO;GAET,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,EAAE,aAAa,OAAO;GAC9D,OAAO,UAAU,KAAA,IAAY,OAAO,MAAM;EAC5C;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flare-engine/leaderboards",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Leaderboard provider interface (consumer-supplied adapter)",
5
5
  "keywords": ["flare-engine", "leaderboards", "provider", "interface"],
6
6
  "license": "Apache-2.0",