@orakl/shared 0.2.0 → 0.3.1

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/badges.d.ts CHANGED
@@ -37,12 +37,14 @@ export type BadgeContext = {
37
37
  /** Rounds this player alone was right with the field answering (Lone torch). */
38
38
  soleCorrectRounds?: number;
39
39
  /**
40
- * This player's rank after each closed round, with the field size then;
41
- * null for rounds before they held a hero slot (Ascendant, Undisputed).
40
+ * This player's rank after each closed round, with the field size then
41
+ * and whether their score was strictly above everyone else's; null for
42
+ * rounds before they held a hero slot (Ascendant, Undisputed).
42
43
  */
43
44
  rankHistory?: Array<{
44
45
  rank: number;
45
46
  field: number;
47
+ lead: boolean;
46
48
  } | null>;
47
49
  };
48
50
  export interface BadgeInput {
@@ -110,7 +112,8 @@ export interface FieldLogEntry {
110
112
  /**
111
113
  * Field-relative round counts for `playerId` from every tenured player's log.
112
114
  * The field for a round is the other players' submitted answers; a miss-filled
113
- * row (a leaver, or a timeout) is not a wrong answer and is left out.
115
+ * row (a leaver, or a timeout) is not a wrong answer and is left out. Round
116
+ * tallies are cached per `logs` map, so `logs` must not change between calls.
114
117
  */
115
118
  export declare function fieldRoundStats(playerId: string, logs: ReadonlyMap<string, readonly FieldLogEntry[]>): {
116
119
  fieldRoundsDefied: number;
package/dist/badges.js CHANGED
@@ -167,7 +167,7 @@ export const BADGES = {
167
167
  "master-of-none": {
168
168
  id: "master-of-none",
169
169
  label: "Many-sided",
170
- description: "Earned four other badges in one run spanning four or more categories.",
170
+ description: "Earned five other badges in one run spanning four or more categories.",
171
171
  icon: "dice",
172
172
  modes: CROSS_MODE,
173
173
  },
@@ -302,7 +302,7 @@ const SURVIVOR_QUESTIONS = 20;
302
302
  /** Distinct categories a run must span for the Many-sided meta-badge. */
303
303
  const MASTER_OF_NONE_MIN_CATEGORIES = 3;
304
304
  /** Other badges that must be earned alongside the breadth for Many-sided. */
305
- const MASTER_OF_NONE_MIN_BADGES = 3;
305
+ const MASTER_OF_NONE_MIN_BADGES = 4;
306
306
  /** Accuracy (0–1) on a hard run that earns giant-slayer. */
307
307
  const GIANT_SLAYER_ACCURACY = 0.9;
308
308
  /** Minimum answers before giant-slayer is meaningful. */
@@ -371,32 +371,64 @@ const LONE_TORCH_MIN_HEROES = 5;
371
371
  const LONE_TORCH_MIN_SUBMITTERS = 4;
372
372
  /** Places Ascendant must climb from the midpoint snapshot. */
373
373
  const ASCENDANT_MIN_CLIMB = 2;
374
+ /** Per-round submitted/correct totals across every log (first row per round). */
375
+ function tallyRounds(logs) {
376
+ const tallies = new Map();
377
+ for (const log of logs.values()) {
378
+ const seen = new Set();
379
+ for (const e of log) {
380
+ if (seen.has(e.questionIndex))
381
+ continue;
382
+ seen.add(e.questionIndex);
383
+ if (!e.submitted)
384
+ continue;
385
+ let t = tallies.get(e.questionIndex);
386
+ if (!t) {
387
+ t = { submitted: 0, correct: 0 };
388
+ tallies.set(e.questionIndex, t);
389
+ }
390
+ t.submitted++;
391
+ if (e.isCorrect)
392
+ t.correct++;
393
+ }
394
+ }
395
+ return tallies;
396
+ }
397
+ // Keyed by the logs map so a final-scores pass tallies once for all players.
398
+ const tallyCache = new WeakMap();
374
399
  /**
375
400
  * Field-relative round counts for `playerId` from every tenured player's log.
376
401
  * The field for a round is the other players' submitted answers; a miss-filled
377
- * row (a leaver, or a timeout) is not a wrong answer and is left out.
402
+ * row (a leaver, or a timeout) is not a wrong answer and is left out. Round
403
+ * tallies are cached per `logs` map, so `logs` must not change between calls.
378
404
  */
379
405
  export function fieldRoundStats(playerId, logs) {
406
+ let tallies = tallyCache.get(logs);
407
+ if (!tallies) {
408
+ tallies = tallyRounds(logs);
409
+ tallyCache.set(logs, tallies);
410
+ }
380
411
  let fieldRoundsDefied = 0;
381
412
  let soleCorrectRounds = 0;
382
- for (const entry of logs.get(playerId) ?? []) {
413
+ const own = logs.get(playerId) ?? [];
414
+ const firstOwn = new Map();
415
+ for (const e of own) {
416
+ if (!firstOwn.has(e.questionIndex))
417
+ firstOwn.set(e.questionIndex, e);
418
+ }
419
+ for (const entry of own) {
383
420
  if (!entry.isCorrect)
384
421
  continue;
385
- let others = 0;
386
- let wrong = 0;
387
- let othersCorrect = 0;
388
- for (const [id, log] of logs) {
389
- if (id === playerId)
390
- continue;
391
- const other = log.find((e) => e.questionIndex === entry.questionIndex);
392
- if (!other?.submitted)
393
- continue;
394
- others++;
395
- if (other.isCorrect)
396
- othersCorrect++;
397
- else
398
- wrong++;
422
+ const t = tallies.get(entry.questionIndex);
423
+ let others = t?.submitted ?? 0;
424
+ let othersCorrect = t?.correct ?? 0;
425
+ const mine = firstOwn.get(entry.questionIndex);
426
+ if (mine?.submitted) {
427
+ others--;
428
+ if (mine.isCorrect)
429
+ othersCorrect--;
399
430
  }
431
+ const wrong = others - othersCorrect;
400
432
  if (others >= CASSANDRA_MIN_OTHERS &&
401
433
  wrong / others >= CASSANDRA_WRONG_SHARE) {
402
434
  fieldRoundsDefied++;
@@ -746,7 +778,7 @@ export function computeBadges(input) {
746
778
  if (victorGates &&
747
779
  context.rank === 1 &&
748
780
  history.length >= 2 &&
749
- history.slice(1).every((h) => h?.rank === 1)) {
781
+ history.slice(1).every((h) => h?.rank === 1 && h.lead)) {
750
782
  eligible.push({ id: "undisputed", detail: "first from round two" });
751
783
  }
752
784
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orakl/shared",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Orakl runtime helpers shared by the web client and the engine.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {