refactor(loldle): store only championNames in KV, recompute rows on render

Round state now keeps `guesses` as a plain string[] (the names the player
tried) instead of caching full comparison results. The board view
rehydrates rows at display time by re-running compareChampions against
the current target.

Smaller KV payloads, and the rendered board always reflects the live
champions.json — useful if a weekly data refresh lands mid-round.
This commit is contained in:
2026-04-22 13:33:46 +07:00
parent df46e4ee22
commit 8992424947
2 changed files with 28 additions and 6 deletions
+21 -3
View File
@@ -55,6 +55,23 @@ function isFinished(game) {
return game.solved || game.giveup || game.guesses.length >= MAX_GUESSES;
}
/**
* Recompute comparison rows for each stored guess against the current target.
* Guesses that no longer resolve (e.g. champion removed from loldle.net) are
* dropped silently — an edge case for stale rounds spanning a data refresh.
*/
function rehydrateGuesses(game) {
const target = champions.find((c) => c.championName === game.target);
if (!target) return [];
const rows = [];
for (const name of game.guesses) {
const guess = champions.find((c) => c.championName === name);
if (!guess) continue;
rows.push({ champion: name, results: compareChampions(guess, target) });
}
return rows;
}
/**
* Load existing round, or create + persist a fresh random one.
* A previously-finished round is discarded and replaced with a fresh one so
@@ -112,13 +129,14 @@ export async function handleLoldle(ctx, db) {
if (!arg) {
const header = `Guess ${game.guesses.length}/${MAX_GUESSES}. Use <code>/loldle &lt;champion&gt;</code>.`;
return ctx.reply(`${header}\n\n${renderBoard(game.guesses)}`, { parse_mode: "HTML" });
const board = renderBoard(rehydrateGuesses(game));
return ctx.reply(`${header}\n\n${board}`, { parse_mode: "HTML" });
}
const guess = findChampion(champions, arg);
if (!guess) return ctx.reply(`Champion not found: "${arg}".`);
if (game.guesses.some((g) => g.champion === guess.championName)) {
if (game.guesses.includes(guess.championName)) {
return ctx.reply(
`🔁 <b>${escapeHtml(guess.championName)}</b> was already guessed this round — try another champion.`,
{ parse_mode: "HTML" },
@@ -134,7 +152,7 @@ export async function handleLoldle(ctx, db) {
);
}
const results = compareChampions(guess, target);
game.guesses.push({ champion: guess.championName, results });
game.guesses.push(guess.championName);
const won = guess.championName === target.championName;
if (won) game.solved = true;
await saveGame(db, subject, game);
+7 -3
View File
@@ -5,8 +5,12 @@
* can /loldle_giveup to reveal (a fresh round auto-starts). Streak = consecutive wins.
*
* Key layout (inside module-prefixed store):
* game:<subject> -> { target, guesses[], solved, giveup, startedAt }
* game:<subject> -> { target, guesses[championName...], solved, giveup, startedAt }
* stats:<subject> -> { played, wins, streak, bestStreak, lastResultAt }
*
* Only championName strings are stored in `guesses` — comparison rows are
* recomputed at render time from the live champions.json. This keeps payloads
* tiny and avoids stale results if loldle.net data shifts mid-round.
*/
const MAX_GUESSES = 8;
@@ -20,8 +24,8 @@ const statsKey = (subject) => `stats:${subject}`;
/**
* @typedef {object} GameState
* @property {string} target — champion id
* @property {Array<{champion:string, results:any[]}>} guesses
* @property {string} target — championName of the hidden champion
* @property {string[]} guesses — championNames already tried this round
* @property {boolean} solved
* @property {boolean} [giveup]
* @property {number} [startedAt] — epoch ms