Daily Bluesky bot for AT Mot. Invites players and congratulates yesterday's solvers.
1import { ORIGIN, type Lang, type YesterdayCounts } from './config.js';
2import { linkFacet, tagFacets, type Facet } from './facets.js';
3
4export interface ComposeInput {
5 lang: Lang;
6 todayN: number;
7 /** Yesterday's counts, or null on day 1 when there is no previous puzzle. */
8 yesterday: YesterdayCounts | null;
9}
10
11export interface ComposedPost {
12 text: string;
13 facets: Facet[];
14 langs: [Lang];
15}
16
17/** Verbatim in the post text; also the idempotency marker. */
18export function postMarker(lang: Lang, todayN: number): string {
19 return lang === 'fr' ? `AT Mot n°${todayN}` : `AT Mot #${todayN}`;
20}
21
22function inviteLine(lang: Lang, todayN: number): string {
23 const marker = postMarker(lang, todayN);
24 return lang === 'fr'
25 ? `🟩 ${marker} est en ligne ! Six essais pour deviner le mot de cinq lettres du jour.`
26 : `🟩 ${marker} is live! Six tries to guess today's five-letter word.`;
27}
28
29/** The congrats sentence, or null when there's nothing meaningful to say. */
30function congratsLine(lang: Lang, c: YesterdayCounts): string | null {
31 const { players, solvers, sampled } = c;
32 if (players == null || players === 0) return null;
33
34 if (solvers === 0) {
35 // An incomplete sample can't prove nobody won — only claim it when we're sure.
36 if (sampled) return null;
37 return lang === 'fr'
38 ? `Personne n'a trouvé le mot d'hier. Nouveau départ aujourd'hui !`
39 : `Nobody cracked yesterday's word. Fresh start today!`;
40 }
41
42 if (sampled) {
43 return lang === 'fr'
44 ? `Bravo aux ${solvers}+ qui ont trouvé le mot d'hier. Saurez-vous les rejoindre aujourd'hui ?`
45 : `Congrats to the ${solvers}+ of you who cracked yesterday's word. Can you join them today?`;
46 }
47
48 const nonSolvers = Math.max(0, players - solvers);
49
50 if (lang === 'fr') {
51 const who = solvers === 1 ? 'au seul joueur' : `aux ${solvers}`;
52 const verb = solvers === 1 ? 'a' : 'ont';
53 let line = `Bravo ${who} qui ${verb} trouvé le mot d'hier.`;
54 if (nonSolvers > 0) {
55 const ns = nonSolvers === 1 ? 'Et au seul qui a séché' : `Et aux ${nonSolvers} qui ont séché`;
56 line += ` ${ns}, meilleure chance aujourd'hui !`;
57 }
58 return line;
59 }
60
61 const who = solvers === 1 ? 'the one player' : `the ${solvers} of you`;
62 let line = `Congrats to ${who} who cracked yesterday's word.`;
63 if (nonSolvers > 0) {
64 const ns = nonSolvers === 1 ? "And to the one who didn't" : `And to the ${nonSolvers} who didn't`;
65 line += ` ${ns}, better luck today!`;
66 }
67 return line;
68}
69
70export function composePost(input: ComposeInput): ComposedPost {
71 const { lang, todayN, yesterday } = input;
72 const congrats = yesterday ? congratsLine(lang, yesterday) : null;
73 const playLine = lang === 'fr' ? `Jouez : ${ORIGIN}` : `Play: ${ORIGIN}`;
74 const tags = lang === 'fr' ? '#JeuDeMots #atproto' : '#WordGame #atproto';
75
76 const text = [inviteLine(lang, todayN), congrats, playLine, tags].filter(Boolean).join('\n');
77 // Homepage link + a #tag facet per hashtag, sorted by position so the byte
78 // ranges appear in document order.
79 const facets = [...linkFacet(text, ORIGIN), ...tagFacets(text)].sort(
80 (a, b) => a.index.byteStart - b.index.byteStart,
81 );
82 return { text, facets, langs: [lang] };
83}