/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.
12 KiB
LoLdle
loldle is a League of Legends "guess the champion" game, after loldle.net's
classic mode. Each guess is scored on seven attributes of the champion, and
the colours say how close it is. It runs as a Telegram HTML5 game behind the
one BotFather game loldle, in two modes:
- Unlimited: each player has their own rounds, each with a random
champion.
/loldlesends the card; the page's New game starts the next round. The chat commands play the same rounds. - LoLdle Daily: one shared champion a day for everyone, new at 07:00 ICT, like Wordle Daily: live group results, a 07:00 recap and push, and a high score on the card.
The two modes share the engine in internal/modules/util/guessgame with
Wordle and Wordle Daily.
Commands
| Command | What it does |
|---|---|
/loldle |
Sends an unlimited card. Without the web game it shows the caller's board in the chat instead |
/loldle <champion> |
Plays the guess on the caller's round: the attribute rows, then Guess n/max., or a sticker, a flavour line, the time and the streak at the end |
/loldle_giveup |
Ends the caller's round as a loss, with the give-up sticker, and reveals the champion |
/loldle_stats |
The caller's unlimited stats |
/loldle_setmax <1-10> |
Owner only. Sets the length of unlimited rounds started in this chat from now on |
/loldledaily |
Sends today's LoLdle Daily card |
/loldledaily_subscribe |
Opts the chat or forum topic into the 07:00 ICT push |
/loldledaily_unsubscribe |
Opts it out |
Champion names are matched loosely: case, spaces and punctuation do not
matter (kaisa is Kai'Sa), and a unique prefix is enough (aat is Aatrox).
An ambiguous prefix, an unknown name and a champion already guessed this
round are refused without using a guess.
Each player has one unlimited round, the same in every chat, and the page and
the chat commands play it together. In a group every member plays their own
round. When a round is over, the next /loldle or /loldle <champion> starts
a new one, as the in-chat game always did. Channels are refused, and so are
anonymous group admins and posts sent on behalf of a chat, as for
Wordle.
Setup
- BotFather: create a game with
/newgameand the short nameloldle. - Public URL: the same
GAME_BASE_URLas the other games. The page is served at<GAME_BASE_URL>/games/loldle/. - Environment:
LOLDLE_GAME_SECRETis optional and must be at least 32 bytes. It derives the link-signing key (tiennm99bot/loldle/token/v1) and the daily answer key (tiennm99bot/loldle/answer/v1). Unset, the bot token is the root key. - Keep
loldleinMODULES, or leaveMODULESempty.
Without GAME_BASE_URL, a long enough secret or storage, the web game is
disabled: /loldle and its helpers still play in the chat, /loldledaily
and /loldledaily_subscribe answer "LoLdle isn't configured on this server.",
Play shows the same alert, and no route or cron exists.
/loldledaily_unsubscribe always works.
The comparison
| Column | Compared | Colours |
|---|---|---|
| Gender | exact | green or red |
| Species | set | green when equal, yellow when they overlap, red otherwise |
| Range type | set | as above |
| Resource | exact | green or red |
| Region(s) | set | as above |
| Position(s) | set | as above |
| Release year | year | green when equal; red with ↑ when the answer is newer, ↓ when older |
The server stores each guess as the champion's name and seven marks in that
order: c correct, p partial, w wrong, and for the year u (answer
newer) or d (answer older). The page and the chat boards rebuild the cells
from the marks and the guessed champion's own values, so the answer's values
never leave the server.
The page
Plain HTML, CSS and JavaScript embedded in the binary, no build step, under
the same strict CSP as the other games plus one image origin,
https://ddragon.leagueoflegends.com, for champion icons. It is dark,
mobile-first and works in Telegram's in-app browsers.
- Header: "LoLdle", the mode (
Daily #N · dateorUnlimited · Round N), the player, and a guess counter. - Search: an ARIA combobox. Typing lists up to 8 champions, those whose
name starts with the text first, then those that contain it, without the
ones already guessed; matching ignores case, spaces and punctuation. Arrow
keys move, Enter or a tap guesses, Escape or a tap outside closes. The list
comes from
champions.json: names and icon ids, nothing else. If it fails to load, the page retries with backoff (1 s doubling to 30 s, and at once on the next keystroke), says so on Enter, and searches the open query again when it arrives. - Board: a Champion column with icon and name, then the seven columns,
newest guess first. On a narrow screen it scrolls sideways with the
Champion column pinned, and cells shrink at 768 and 480 pixels. A new row's
cells appear one after another (80 ms apart), except under
prefers-reduced-motion. - Icons:
https://ddragon.leagueoflegends.com/cdn/img/champion/tiles/<id>_0.jpg. An icon that fails to load is replaced by the champion's initial. - Screen readers: each cell is labelled with its column, value, result and, for the year, which way the answer lies; a new guess is announced the same way.
- End panel: "You got it!" with the guess count, or "Game Over" with the
champion's icon, name and title; then the stats and guess distribution.
Daily adds a countdown to 07:00 ICT, Share and Copy result;
unlimited adds New game and a pointer to
/loldledaily. - New game in the header asks for a second tap when the round has guesses.
Unlimited rounds
- Length: a round allows 8 guesses unless the chat it starts in has a
/loldle_setmaxvalue (1–10). A round keeps the length it started with.- From a command, the chat is the command's chat (a group, or the private chat).
- From the page (its first round, or New game), the chat is the card's. An inline card uses 8.
- New game and an abandoned round follow unlimited Wordle: giving up a round with a guess counts as a loss and the page shows its champion, an unguessed round is replaced, and a stale round number starts nothing.
- Stats: played, wins, current streak (consecutive wins), best streak and the guess distribution (up to 10).
- No high score: unlimited wins are not reported with
setGameScore; a round's length can differ by chat, so the scores would not compare. - Data refresh: a round whose champion was removed from the data is replaced without counting.
Which card opens which mode works as for Wordle: /loldle records its cards
(ucard:<chat>:<message>, refreshed once a day when played), and every other
card, /loldledaily, the push, an inline share, a card unplayed for 30 days or
a forwarded copy of a /loldle card, opens the daily champion. The cron loldle_unlimited_cards (40 20 * * *
UTC) forgets old records. See unlimited Wordle.
LoLdle Daily
Everything works as in Wordle Daily, with these values:
- Puzzle numbering: puzzle #1 is 9 October 2026, the same epoch as Wordle Daily, so both games share their day number.
- Answers: the champions sorted by name, walked in a keyed permutation per
cycle. The first resolution of a day pins its champion as
puzzle:<n>, so adding champions or changing the secret only affects days not started yet. If a data refresh removes a champion that is already pinned, that day stays playable: its attributes are unknown, so every guess scores 🟥 in every column and the board ends in a loss that reveals the name (naming the champion still wins, but the page's search no longer lists it). - Guesses: 8. A win scores
9 − guesseswithsetGameScore. - Live results: one message per group topic and day, with one row of 7 squares per guess (🟩 correct, 🟨 partial, 🟥 wrong). No champion is named.
- Push: the cron
loldledaily_daily_push(0 0 * * *UTC). The recap opens with yesterday's champion, then the group streak (wins only), then the finishers grouped by guess count:
LoLdle Daily #11 — yesterday's results
Champion: Ahri
🔥 Your group is on a 3 day streak!
👑 2/8: Alice, Dan
5/8: Eve
X/8: Bob
- Share text:
LoLdle Daily #11 2/8and the square rows.
HTTP API
Routes under /games/loldle/:
| Route | Body | Answer |
|---|---|---|
GET /, app.js, app.css |
the page | |
GET champions.json |
[{"name","id"}], cached for an hour |
|
POST api/state |
{"token"} |
view |
POST api/guess |
{"token","num"} (daily) or {"token","seq"} (unlimited), plus "name" |
view |
POST api/new |
{"token","seq"} |
the next unlimited round; 403 bad_mode on a daily card |
The view has mode, player, max, columns ({key, label} ×7),
guesses and status; daily adds num, date and next_at, unlimited adds
seq and gave_up, and after api/new gives up a round with guesses,
abandoned ({name, id, title}) names its champion. Each guess is {name, id, marks, cells}, a cell being
{key, value, result, dir} with the guessed champion's own value. answer
({name, id, title}), stats and the daily share appear only once the game
is over.
Errors are {"error","message"}: 400 bad_request, 401 bad_token /
expired, 403 bad_mode, 409 new_puzzle / new_round, 422 unknown /
ambiguous / duplicate / finished, 429 rate_limited (60 requests a
minute per player).
Storage
All in the loldle collection:
| Key | Holds |
|---|---|
config:<chat> |
the chat's /loldle_setmax value |
uround:<user>, ustats:<user>, ucard:<chat>:<message> |
unlimited rounds, stats and cards |
puzzle:<n>, play:<n>:<user>, dstats:<user>, cday:…, cstreak:…, subscribers, daily_push:last_date |
LoLdle Daily, as Wordle Daily's keys; the stats prefix is dstats: because stats: holds the in-chat game's old stats |
game:<subject>, stats:<subject> |
the in-chat game before unlimited mode; read once by the migration, never written |
Carrying over the in-chat game
At startup loldle.InitStore copies the in-chat game's players into
unlimited mode, once (marker loldle:legacy-unlimited in the system
collection), whatever MODULES says:
stats:<user>becomes the player's unlimited stats (streak→ current,bestStreak→ best); the distribution starts at zero.game:<user>, a round still being played (the old game deleted finished ones), becomes the player's current round, with its frozen length and start time. Its colours are rebuilt from today's champion data; a guess naming a champion no longer in the data is dropped, and a round whose answer is gone is not carried.- Group rounds and group stats (negative subjects) belong to no single player and are not carried.
- Nothing already there is overwritten, and the old documents stay, so a rollback is a redeploy.
Champion data
internal/modules/loldle/data/champions.json holds 172 champions in
loldle.net's schema (gender, positions, species, resource, range type,
regions, release date). id (the Data Dragon key, which names the icon, e.g.
MonkeyKing for Wukong) and title were added by name from
tiennm99/loldle's Data Dragon scrape. Data Dragon's tile files are
case-sensitive and one does not match its key: Fiddlesticks' tile is
FiddleSticks_0.jpg, so tileIDs in champions.go overrides that id at
load. Tests check that every champion has a unique, letters-only id and a
title, and that the override applies.
Locke, in that scrape but not here, is not added yet: this data needs his species, positions and release date, which the scrape does not have.