/wordle and /loldle now send the wordle and loldle BotFather games in an unlimited mode: each player gets their own word or champion, saved after every guess, with a New game button. Cards the bot did not record as unlimited, including the daily cards, play the daily puzzle. The helper commands keep working on the player's round. /loldledaily mirrors /wordledaily: one champion per day at 07:00 ICT, subscribe and unsubscribe, the morning recap and the live group results. The daily and unlimited machinery is shared in util/guessgame, the old in-chat wordle module is removed, and a one-time startup migration carries each player's stats and unfinished round over without changing old data.
15 KiB
Wordle Daily
wordledaily is a Telegram HTML5 game modelled on NYT Wordle and its Discord
Activity. Everyone plays the same English five-letter word each day. A new
puzzle starts at 07:00 ICT (00:00 UTC), and the puzzle number goes up by one
each day.
The same module and BotFather game also run unlimited Wordle:
/wordle sends a card that opens the player's own random rounds. Play
decides the mode from the card; see Card modes.
The daily game has three commands:
/wordledailysends the game card in any chat, private or group, and in a forum topic it stays in that topic. Pressing Play opens a board page that this bot serves./wordledaily_subscribeopts the chat, or the forum topic, into a daily push at 07:00 ICT./wordledaily_unsubscribeopts it out.
A topic means a real forum topic (is_topic_message). Telegram also gives a
reply chain in an ordinary supergroup a thread ID; a command sent as such a
reply counts for the whole chat, the same scope its plays and group results
are stored under.
Anyone in a chat may subscribe or unsubscribe, the same as /lol_subscribe and
/thuyvan_subscribe. Channels are refused, because Telegram does not allow
games in channels.
The word data and scoring live in internal/modules/wordle/wordlist. The
daily engine described here (puzzles, progress, stats, live results, recap
and push) lives in internal/modules/util/guessgame and is shared with
LoLdle Daily.
Card modes
Both modes use the one BotFather game wordle, so a card does not say which
mode it is. /wordle and /wordle_new record each card they send; Play on a
recorded card opens unlimited mode, and every other card opens today's
puzzle:
/wordledailycards and the 07:00 push,- every card sent before unlimited mode existed,
- inline shares (
t.me/<bot>?game=wordle), which have no chat message to record, /wordlecards nobody played for 30 days, whose record expired,- forwarded copies of a
/wordlecard, which the bot cannot trace back to their record.
A failed lookup answers Play with an alert rather than opening the wrong mode. See unlimited Wordle.
Setup
-
BotFather: create a game for the bot with
/newgameand the short namewordle. The share link is thent.me/<bot username>?game=wordle. The module, its commands and its routes keep the namewordledaily; only the BotFather game iswordle. -
Public URL: this uses the same
GAME_BASE_URLas the noitu game. The page is served at<GAME_BASE_URL>/games/wordledaily/. -
Environment:
WORDLEDAILY_GAME_SECRETis optional and must be at least 32 bytes. Two keys are derived from it, each with its own label:tiennm99bot/wordledaily/token/v1signs the Play links.tiennm99bot/wordledaily/answer/v1orders the daily answers.
When the secret is unset, the bot token is the root key. Because the labels are separate, a noitu link never opens this game.
-
Keep
wordledailyinMODULES, or leaveMODULESempty. AMODULESvalue that still names the retiredwordlemodule loadswordledaily.
The game is disabled when any of these holds:
GAME_BASE_URLis unset or invalid.- The secret is shorter than 32 bytes.
- There is no storage.
A disabled game behaves like this:
/wordledailyand/wordledaily_subscribeanswer "Wordle Daily isn't configured on this server."/wordledaily_unsubscribestill removes a subscription./wordleshows the unlimited round's board in the chat instead of a card; the other/wordlecommands keep working in the chat.- Play shows the same text as an alert.
- No
/games/wordledaily/route exists and the daily push is not registered.
The puzzle
- Day boundary: puzzle #1 started at 07:00 ICT on 9 October 2026. Puzzle
nruns fromepoch + (n−1) daysuntil one day later. Because 07:00 ICT is 00:00 UTC, the 00:00 UTC cron always pushes exactly the puzzle a player then gets. - Word lists:
wordlist/data/answers.txtholds about 2,230 common English words that a puzzle can be. It was curated for this bot; it is not NYT's list.wordlist/data/words.txt(14,855 words) is what a guess may be.- A test checks that the answers are sorted, unique, and all in the dictionary.
- Choosing the answer: the days are split into cycles as long as the answer
list. Each cycle walks one permutation of the list: a Fisher–Yates shuffle
seeded with
HMAC(answer key, "cycle:<n>")through ChaCha8.- No answer repeats within a cycle (about six years).
- The order can't be guessed without the key.
- Pinning: the first time a puzzle is needed (a request, or the push at
07:00), its answer is stored as
puzzle:<n>. Every later read uses that document, so a new secret or an edited answer list never changes a puzzle that has already started. - Rollover mid-game: a guess names the puzzle number the page shows. A
guess for a puzzle that is no longer current is refused with
new_puzzle, and the page reloads the new board.
Playing
- Validation: the server checks every guess and colours it. Two-pass marking handles duplicate letters the NYT way. The page never receives a word list, and it never receives the answer until that player's game is over.
- Progress: each player's board is saved after every guess, per puzzle. A restart, a reload, or opening any other daily card (see Card modes) shows the same board.
- One game a day: each player gets one game per day across all chats. A finished puzzle can't be replayed.
- Stats: each player's stats cover every chat. They are played, win %,
current and max streak, and the guess distribution, shown on the end screen.
- The current streak counts consecutive puzzle wins.
- A loss or a missed day ends it.
- A streak whose last win is older than yesterday shows as 0.
- End screen: it shows the answer, the stats, a countdown to the next
puzzle and two buttons:
- Share uses Telegram's game share.
- Copy result copies the spoiler-free text, such as
Wordle Daily #12 3/6followed by the emoji grid.
- Page features: physical and on-screen keyboards, a dark scheme that follows
the system, and a high-contrast (orange/blue) toggle that is remembered in
localStorage. The board is sized from the height the header and keyboard leave, so the page fits a short phone screen without scrolling. Each tile turns to its colour in turn, and the keyboard colours once the row is revealed. Tiles and keys carry their result as an accessible label, and each guess's result is announced to screen readers. - Score: a win reports
7 − guesseswithsetGameScore(force=false). A 1-guess win scores 6 and a 6-guess win scores 1. A loss is not reported.- The score is set on the card the player played from.
- When they open another card the same day, it is set there too, once per card.
- Each day's card therefore carries that day's leaderboard.
- A card is recorded as reported only once Telegram accepts the score.
BOT_SCORE_NOT_MODIFIEDcounts as accepted. Any other failure is logged, and the next time the player opens that card the score is set again.
Groups: mapping the Discord Activity to Telegram
NYT's Discord Activity works like this:
- Everyone plays the same daily word.
- A live embed in the channel shows each player's progress.
- Results are spoiler-free colour grids.
- A daily message recaps yesterday's scores, crowns the best, and shows the group streak.
- Reminders of a new puzzle can be muted.
Telegram has different limits:
- A game message has no caption, so any recap has to be a separate text message.
- Bots can edit their own text messages.
setGameScorein a group also posts Telegram's own "X scored N" service message, which cannot be avoided.- A message holds at most 4,096 characters, and groups allow about 20 messages a minute.
The bot maps the Activity onto those limits as follows.
| Discord Activity | Here |
|---|---|
/wordle launches the activity |
/wordledaily sends the card; Play opens the board |
| Live channel embed of everyone's progress | One live results message per group, topic and day (below) |
/share and the Share button |
Share and Copy result on the end screen |
| Daily recap of yesterday, crown, group streak | The 07:00 ICT push to subscribed chats (below) |
| Mute daily reminders | /wordledaily_unsubscribe |
| Personal streaks | Per-user stats, the same for every chat |
Live results message
A live results message exists only in groups and supergroups, never in a private chat or for an inline card. It works like this:
-
Who appears: a player is listed in a group topic's day once they have made a guess from a card in that topic. A player who finished elsewhere and then opens the group's card is listed at once, with their finished grid.
-
Creating it: the first time, the bot sends one silent message as a reply to the card, with
AllowSendingWithoutReply. After that it only edits that message. -
Rate of updates: edits are coalesced, at most one every 5 seconds per message, and the text is always rendered from the store.
-
Errors:
- A "message is not modified" answer is ignored.
- If the message was deleted, a new one is sent at most once every 10 minutes. An update that comes sooner is held until then, so the final results still arrive.
- A failure that may pass (429, a timeout, a 5xx, a store error) is retried
up to 5 times, waiting Telegram's
retry_afteror 5, 10, 20, 40 and 80 seconds. A refused request (bad request, forbidden) is not retried.
-
Content: colour grids only, never letters. The rows are ordered like this:
- Winners, by fewer guesses and then earlier finish.
- Losses (
X/6). - Players still playing, with their partial grid, most guesses first.
The message stops before Telegram's limit and ends with
+N more.
Wordle Daily #12 · live results
🔥 Group streak: 5 days
Alice 3/6
⬜🟨⬜⬜⬜
🟨🟩⬜🟩⬜
🟩🟩🟩🟩🟩
Bob X/6
…
Carol playing 2/6
⬜⬜🟨⬜⬜
⬜🟩🟩⬜⬜
The 07:00 ICT push
The cron wordledaily_daily_push (0 0 * * * UTC) does the following:
- Pins today's answer.
- Deletes the
puzzle:,play:andcday:documents of puzzles older than yesterday's (see Storage). - Claims the day under
daily_push:last_date, so a second trigger on the same day sends nothing. - For each subscriber:
- In a group, it sends yesterday's recap. The recap opens with yesterday's answer, then the group streak, then the players who finished.
- It sends today's card in the same topic.
If the recap fails to send, the card is skipped. The shared subscription fan-out then prunes chats that blocked or removed the bot. Sends are throttled above 30 subscribers.
Wordle Daily #11 — yesterday's results
Answer: CRANE
🔥 Your group is on a 5 day streak!
👑 3/6: Alice, Dan
4/6: Eve
X/6: Bob
What else the push sends depends on the day before:
- Group streak: it counts consecutive puzzles that at least one member of
the group or topic solved; a loss does not keep it going. If nobody
solved yesterday's word and a streak of at least 2 days was alive, the
streak line reads
Nobody solved it. Group streak reset. - Nobody played: the recap is just the title and the answer.
- Private chats: a private chat always gets just the card.
Streak lines appear only from 2 days up.
Nothing else is posted. There is no separate "X is playing" message and no message per guess; the live results message covers both.
HTTP API
Every route lives under /games/wordledaily/:
GET /games/wordledaily/serves the page, andGET app.jsandGET app.cssserve its assets.- The API routes are
POSTwith a JSON body.
Headers, CSP, caching and the 4 KB request limit are the same as noitu's,
through internal/modules/util/htmlgame.
| Route | Body | Answer |
|---|---|---|
api/state |
{"token"} |
view |
api/guess |
{"token","num","word"} |
view |
api/new |
{"token","seq"} |
unlimited cards only; see unlimited Wordle |
The token decides the mode. The daily view has these fields:
mode(daily),num,date,player,max(6),len(5)guesses: a list of{word, marks}.markshas one letter per position:ccorrect,ppresent elsewhere,wwrong.status:playing,wonorlostnext_at: the unix second the next puzzle startsanswer,stats(played,win_pct,cur,max,dist[6]) andshare: only once the game is over
Errors are {"error","message"}:
| Status | Codes |
|---|---|
| 400 | bad_request |
| 401 | bad_token, expired |
| 403 | bad_mode (api/new on a daily card) |
| 409 | new_puzzle, new_round (unlimited) |
| 422 | length, unknown, finished |
| 429 | rate_limited: 60 requests a minute per player, every route together |
A Play token expires after 6 hours and is not tied to a day.
Storage
Everything is in the module's collection, under separate key prefixes.
| Key | Holds |
|---|---|
puzzle:<n> |
the pinned answer of puzzle n |
play:<n>:<user> |
one player's board on puzzle n: guesses with marks, status, finish time, cards reported on, group topics joined |
stats:<user> |
played, wins, current and max streak, guess distribution, last puzzle finished and won |
cday:<n>:<chat>:<thread>: |
the players of a group topic's day, the card the summary replies to, and the summary message ID |
cstreak:<chat>:<thread>: |
the group topic's streak and the last puzzle it counted |
subscribers |
the subscribed chats and topics |
daily_push:last_date |
the last puzzle number pushed |
uround:, ustats:, ucard: |
unlimited mode; see unlimited Wordle |
Each player's board and stats are written under a per-player lock. The board is the record and is written first. A stats update is skipped for a puzzle already counted, and when the stats write after a finished board fails, the next state load or guess folds that board into the stats. So the stats always follow the board and never count a game twice. Group documents use versioned writes with retries.
Retention: the 07:00 push deletes puzzle:<n>, play:<n>:* and
cday:<n>:* for every n older than yesterday's puzzle; nothing reads them
after the recap. stats:, cstreak: and the subscription documents are
kept. With the memory store this keeps the process's memory bounded.
Choices made
These follow NYT's behaviour where it is known. Change them here if wanted:
- The recap shows yesterday's answer first.
- The group streak counts wins only.
- The live results message shows partial grids of players still playing, as NYT's live embed does. This reveals some colour hints to members who have not played yet.
- The answer list was curated for this bot and can be replaced. A puzzle that has already started keeps its stored answer.