mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-05-05 03:36:39 +00:00
feat(wordle): render board in monospace so markers align with letters
Wrap renderGuess / renderBoard output in <pre> and send replies with
parse_mode: HTML. In Telegram's monospace font each " X " letter cell
is 3 characters wide, which is roughly the width of a single emoji
marker, so colored squares stack cleanly over the letter they score.
No user-controlled content lands inside the <pre> (guesses are
validated [a-z]{5}), so no HTML escaping is needed in the grid. The
inline <code>/wordle <word></code> placeholder is properly
entity-encoded for HTML parse mode.
This commit is contained in:
@@ -24,6 +24,10 @@ import wordsData from "./words-data.js";
|
||||
const words = wordsData;
|
||||
const wordSet = makeWordSet(words);
|
||||
|
||||
// All replies use HTML parse mode so renderGuess/renderBoard <pre> blocks
|
||||
// render as a monospace code box; emoji markers then align with letter columns.
|
||||
const HTML = { parse_mode: "HTML" };
|
||||
|
||||
/**
|
||||
* @param {import("grammy").Context} ctx
|
||||
* @returns {number|null}
|
||||
@@ -86,8 +90,8 @@ export async function handleWordle(ctx, db) {
|
||||
? `🎉 Solved in ${game.guesses.length}/${MAX_GUESSES}. /wordle_new for another.`
|
||||
: game.giveup
|
||||
? `🏳️ Gave up. Answer was ${game.target.toUpperCase()}. /wordle_new for another.`
|
||||
: `Guess ${game.guesses.length}/${MAX_GUESSES}. Use \`/wordle <word>\`.`;
|
||||
return ctx.reply(`${header}\n\n${renderBoard(game.guesses)}`);
|
||||
: `Guess ${game.guesses.length}/${MAX_GUESSES}. Use <code>/wordle <word></code>.`;
|
||||
return ctx.reply(`${header}\n\n${renderBoard(game.guesses)}`, HTML);
|
||||
}
|
||||
|
||||
if (isFinished(game)) {
|
||||
@@ -110,15 +114,17 @@ export async function handleWordle(ctx, db) {
|
||||
const s = await recordResult(db, subject, true);
|
||||
return ctx.reply(
|
||||
`${reply}\n\n🎉 Solved in ${game.guesses.length}/${MAX_GUESSES}! Streak: ${s.streak}. /wordle_new for another.`,
|
||||
HTML,
|
||||
);
|
||||
}
|
||||
if (game.guesses.length >= MAX_GUESSES) {
|
||||
await recordResult(db, subject, false);
|
||||
return ctx.reply(
|
||||
`${reply}\n\n❌ Out of guesses. Answer was ${game.target.toUpperCase()}. /wordle_new to retry.`,
|
||||
HTML,
|
||||
);
|
||||
}
|
||||
return ctx.reply(`${reply}\n\nGuess ${game.guesses.length}/${MAX_GUESSES}.`);
|
||||
return ctx.reply(`${reply}\n\nGuess ${game.guesses.length}/${MAX_GUESSES}.`, HTML);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +143,10 @@ export async function handleNew(ctx, db) {
|
||||
}
|
||||
|
||||
await startFreshGame(db, subject);
|
||||
return ctx.reply(`${prelude}🆕 New round started. Use \`/wordle <word>\` to guess.`);
|
||||
return ctx.reply(
|
||||
`${prelude}🆕 New round started. Use <code>/wordle <word></code> to guess.`,
|
||||
HTML,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
/**
|
||||
* @file Render wordle comparison results as a Telegram-friendly grid.
|
||||
* @file Render wordle comparison results as a Telegram monospace grid.
|
||||
*
|
||||
* Each guess renders as two lines — colored markers over the upper-case
|
||||
* letters — wrapped in an HTML <pre> block so Telegram renders it in
|
||||
* monospace and the emoji column widths line up with the letter columns.
|
||||
*
|
||||
* 🟩 correct · 🟨 partial · ⬜ wrong
|
||||
*
|
||||
* Each guess shows two lines: the colored markers and the upper-case letters
|
||||
* underneath so the player can read the word at a glance.
|
||||
* Output strings are intended to be sent with `parse_mode: "HTML"`. All
|
||||
* game content is validated `[a-z]` so no HTML escaping is needed inside
|
||||
* the grid; only callers embedding user-controlled text around the grid
|
||||
* need to escape it.
|
||||
*/
|
||||
|
||||
const MARKER = { correct: "🟩", partial: "🟨", wrong: "⬜" };
|
||||
|
||||
/**
|
||||
* Render a single guess row (two lines: markers + letters).
|
||||
* @param {ReturnType<import("./compare.js").compareWords>} results
|
||||
*/
|
||||
export function renderGuess(results) {
|
||||
function rowPair(results) {
|
||||
const markers = results.map((r) => MARKER[r.result] ?? "⬜").join("");
|
||||
const letters = results.map((r) => ` ${r.letter.toUpperCase()} `).join("");
|
||||
return `${markers}\n${letters}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all prior guesses stacked.
|
||||
* Render a single guess row as an HTML <pre> block.
|
||||
* @param {ReturnType<import("./compare.js").compareWords>} results
|
||||
*/
|
||||
export function renderGuess(results) {
|
||||
return `<pre>${rowPair(results)}</pre>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the full board (all prior guesses, blank-line separated) as a
|
||||
* single HTML <pre> block, so all rows share one monospace code box.
|
||||
* @param {Array<{word:string, results: any[]}>} guesses
|
||||
*/
|
||||
export function renderBoard(guesses) {
|
||||
if (guesses.length === 0) return "No guesses yet. Reply with `/wordle <word>`.";
|
||||
return guesses.map((g) => renderGuess(g.results)).join("\n\n");
|
||||
if (guesses.length === 0) return "No guesses yet. Reply with <code>/wordle <word></code>.";
|
||||
return `<pre>${guesses.map((g) => rowPair(g.results)).join("\n\n")}</pre>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user