mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-13 22:16:33 +00:00
fix(loldle): start round timer on first guess, not round end
Previously startFreshGame was called at the tail of every win/lose/giveup path, stamping startedAt to that moment — so the clock accrued while the player was away between rounds. Now: - round-ending paths call clearGame (new helper in state.js), deleting the KV record instead of pre-creating the next round - getOrInitGame lazily creates the next round on the player's next /loldle call, with startedAt: null - the first actual guess inside handleLoldle stamps startedAt = Date.now() Viewing an empty board gives no hints, so it shouldn't count against the clock. handleGiveup no longer auto-creates a fresh round and now reports "No active round" when called with nothing in progress.
This commit is contained in:
1 parent
cedd9da4ac
commit
ba829d2906
3 files changed
+47
-18
No files matched your search
@@ -26,13 +26,15 @@ opens a PR whenever the data changes.
|
||||
| Command | Visibility | Description |
|
||||
|---------|-----------|-------------|
|
||||
| `/loldle` | public | Show current board, or submit a champion guess |
|
||||
| `/loldle_giveup` | public | Reveal the answer and auto-start a fresh round |
|
||||
| `/loldle_giveup` | public | Reveal the answer and end the round |
|
||||
| `/loldle_stats` | public | Show your wins / streak |
|
||||
|
||||
Submit a guess with `/loldle <champion>` (e.g. `/loldle Ahri`). Names match
|
||||
case/space/punctuation-insensitive with a unique-prefix fallback. A round
|
||||
that ends (solved, gave up, or out of guesses) is immediately replaced by a
|
||||
fresh one — no manual "new round" command.
|
||||
that ends (solved, gave up, or out of guesses) is cleared; the next round
|
||||
is created lazily on the first `/loldle` call, and the timer (`startedAt`)
|
||||
only starts when the player submits their first actual guess — viewing an
|
||||
empty board gives no hints, so it shouldn't count against the clock.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
* private chat → user id (per-user game)
|
||||
* group/supergroup chat → chat id (shared game — everyone plays together)
|
||||
*
|
||||
* Round lifecycle: a round is created lazily on the first /loldle call after
|
||||
* the previous round ended, and the timer (`startedAt`) only starts when the
|
||||
* player submits their first actual guess — viewing an empty board gives no
|
||||
* hints, so it shouldn't count against the clock.
|
||||
*
|
||||
* Commands:
|
||||
* /loldle → show the current board (or start a round)
|
||||
* /loldle <champion> → submit a guess
|
||||
* /loldle_giveup → reveal the answer and auto-start a fresh round
|
||||
* /loldle_giveup → reveal the answer and end the round
|
||||
* /loldle_stats → show wins / streak for the current subject
|
||||
*/
|
||||
|
||||
@@ -18,13 +23,14 @@ import { compareChampions } from "./compare.js";
|
||||
import { attemptFlavor, formatDuration } from "./flavor.js";
|
||||
import { findChampion } from "./lookup.js";
|
||||
import { renderBoard, renderGuess } from "./render.js";
|
||||
import { MAX_GUESSES, loadGame, loadStats, recordResult, saveGame } from "./state.js";
|
||||
import { MAX_GUESSES, clearGame, loadGame, loadStats, recordResult, saveGame } from "./state.js";
|
||||
import { GIVEUP_STICKERS, LOSE_STICKERS, WIN_STICKERS, pickSticker } from "./stickers.js";
|
||||
|
||||
/** @type {Array<Record<string, any>>} */
|
||||
const champions = championsData;
|
||||
|
||||
const NEW_ROUND_HINT = "🆕 New round started. Use <code>/loldle <champion></code> to guess.";
|
||||
const NEW_ROUND_HINT =
|
||||
"🆕 Send <code>/loldle</code> or <code>/loldle <champion></code> to start a new round.";
|
||||
|
||||
function getSubject(ctx) {
|
||||
const type = ctx.chat?.type;
|
||||
@@ -61,7 +67,9 @@ function rehydrateGuesses(game) {
|
||||
|
||||
async function startFreshGame(db, subject) {
|
||||
const target = pickRandomChampion();
|
||||
const fresh = { target: target.championName, guesses: [], startedAt: Date.now() };
|
||||
// startedAt stays null until the player actually submits their first guess —
|
||||
// seeing an empty board gives no hints, so the round hasn't really begun.
|
||||
const fresh = { target: target.championName, guesses: [], startedAt: null };
|
||||
await saveGame(db, subject, fresh);
|
||||
return fresh;
|
||||
}
|
||||
@@ -113,13 +121,15 @@ export async function handleLoldle(ctx, db) {
|
||||
const target = findByName(game.target);
|
||||
// champions.json can be refreshed between rounds — an active target may disappear.
|
||||
if (!target) {
|
||||
await startFreshGame(db, subject);
|
||||
return ctx.reply(
|
||||
"Champion data was updated since this round started. Starting a fresh round — try again.",
|
||||
);
|
||||
await clearGame(db, subject);
|
||||
return ctx.reply(`Champion data was updated since this round started. ${NEW_ROUND_HINT}`, {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
}
|
||||
|
||||
const results = compareChampions(guess, target);
|
||||
// Stamp the clock on the first guess — prior /loldle views don't count.
|
||||
if (game.startedAt == null) game.startedAt = Date.now();
|
||||
game.guesses.push(guess.championName);
|
||||
const won = guess.championName === target.championName;
|
||||
|
||||
@@ -129,7 +139,7 @@ export async function handleLoldle(ctx, db) {
|
||||
|
||||
if (won) {
|
||||
const s = await recordResult(db, subject, true);
|
||||
await startFreshGame(db, subject);
|
||||
await clearGame(db, subject);
|
||||
await trySendSticker(ctx, WIN_STICKERS);
|
||||
const flavor = attemptFlavor(game.guesses.length, MAX_GUESSES);
|
||||
return ctx.reply(
|
||||
@@ -140,7 +150,7 @@ export async function handleLoldle(ctx, db) {
|
||||
|
||||
if (game.guesses.length >= MAX_GUESSES) {
|
||||
await recordResult(db, subject, false);
|
||||
await startFreshGame(db, subject);
|
||||
await clearGame(db, subject);
|
||||
await trySendSticker(ctx, LOSE_STICKERS);
|
||||
return ctx.reply(`${reply}\n\n❌ Out of guesses. Answer was ${champ}.\n${NEW_ROUND_HINT}`, {
|
||||
parse_mode: "HTML",
|
||||
@@ -160,12 +170,16 @@ export async function handleLoldle(ctx, db) {
|
||||
export async function handleGiveup(ctx, db) {
|
||||
const subject = getSubject(ctx);
|
||||
if (subject == null) return ctx.reply("Cannot identify chat.");
|
||||
const game = await getOrInitGame(db, subject);
|
||||
const existing = await loadGame(db, subject);
|
||||
// No active round — nothing to give up on.
|
||||
if (!existing) {
|
||||
return ctx.reply(`No active round. ${NEW_ROUND_HINT}`, { parse_mode: "HTML" });
|
||||
}
|
||||
await recordResult(db, subject, false);
|
||||
const target = findByName(game.target);
|
||||
await startFreshGame(db, subject);
|
||||
const target = findByName(existing.target);
|
||||
await clearGame(db, subject);
|
||||
await trySendSticker(ctx, GIVEUP_STICKERS);
|
||||
const answer = target ? escapeHtml(target.championName) : escapeHtml(game.target);
|
||||
const answer = target ? escapeHtml(target.championName) : escapeHtml(existing.target);
|
||||
return ctx.reply(`🏳️ Answer was ${answer}.\n${NEW_ROUND_HINT}`, { parse_mode: "HTML" });
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ const statsKey = (subject) => `stats:${subject}`;
|
||||
* @typedef {object} GameState
|
||||
* @property {string} target — hidden champion's championName
|
||||
* @property {string[]} guesses — championNames already tried this round
|
||||
* @property {number} startedAt — epoch ms
|
||||
* @property {number|null} startedAt — epoch ms; null until the first guess is
|
||||
* submitted (viewing an empty board shouldn't start the clock)
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,18 @@ export async function saveGame(db, subject, state) {
|
||||
await db.putJSON(gameKey(subject), state, { expirationTtl: GAME_TTL_SECONDS });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the stored game so the next /loldle call creates a fresh round.
|
||||
* Used after win / loss / giveup — the new round's timer should start on the
|
||||
* player's next interaction, not at the moment the previous round ended.
|
||||
*
|
||||
* @param {import("../../db/kv-store-interface.js").KVStore} db
|
||||
* @param {number|string} subject
|
||||
*/
|
||||
export async function clearGame(db, subject) {
|
||||
await db.delete(gameKey(subject));
|
||||
}
|
||||
|
||||
export { MAX_GUESSES };
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user