Daily Bluesky bot for AT Mot. Invites players and congratulates yesterday's solvers.
1import { SOLVER_SAMPLE_CAP, type Lang, type ResultRecord, type YesterdayCounts } from './config.js';
2import { dailyPlayerCount, dailyResultBacklinks, backlinkUri } from './constellation.js';
3import { getRecordByUri } from './identity.js';
4
5/**
6 * Yesterday's { players, solvers, sampled } for one language. `players` is the
7 * cheap distinct-DID count; `solvers` is the number of *distinct* DIDs whose
8 * sampled record (≤ SOLVER_SAMPLE_CAP) shows a win.
9 *
10 * Both numbers are deduped by DID so they are commensurable, and `solvers` is
11 * clamped to `players` so the published copy can never say more people solved
12 * than played. `sampled` is true whenever the sample is incomplete — the page
13 * was truncated, or we resolved fewer distinct players than Constellation
14 * counted (a dropped read or index skew) — so compose hedges instead of
15 * publishing an untrustworthy exact "X solved, Y didn't".
16 */
17export async function yesterdayCounts(lang: Lang, yesterdayN: number): Promise<YesterdayCounts> {
18 const [players, { records, truncated }] = await Promise.all([
19 dailyPlayerCount(lang, yesterdayN),
20 dailyResultBacklinks(lang, yesterdayN, SOLVER_SAMPLE_CAP),
21 ]);
22
23 const seenDids = new Set<string>();
24 const solverDids = new Set<string>();
25 await Promise.all(
26 records.map(async (bl) => {
27 const rec = await getRecordByUri<ResultRecord>(backlinkUri(bl));
28 if (!rec || rec.lang !== lang || rec.puzzleNumber !== yesterdayN) return;
29 seenDids.add(bl.did);
30 if (rec.solved) solverDids.add(bl.did);
31 }),
32 );
33
34 let solvers = solverDids.size;
35 if (players != null) solvers = Math.min(solvers, players);
36
37 const sampled = truncated || players == null || (players > 0 && seenDids.size < players);
38
39 return { players, solvers, sampled };
40}