mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-18 08:21:26 +00:00
feat(doantu): add /doantu_hint — 3 warm-but-not-hot related words
Uses phow2sim /neighbors. Filters out capitalized foreign place names that leak in from the corpus (e.g. al-Qantara, Nam_Afrin) and requires tokens look Vietnamese (diacritic or underscore compound) to dodge pure-ASCII junk like "adiyeh". Samples 3 from the tail after skipping the top 20% so the hint doesn't give away the answer.
This commit is contained in:
@@ -13,6 +13,7 @@ native `/` autocomplete menu.
|
||||
| Command | Visibility | Description |
|
||||
|---------|-----------|-------------|
|
||||
| `/doantu` | public | Show current board or submit a word guess |
|
||||
| `/doantu_hint` | public | Reveal 3 related words (not the answer) as a nudge |
|
||||
| `/doantu_giveup` | public | Reveal the answer and end the round (next `/doantu` starts a fresh one) |
|
||||
| `/doantu_stats` | public | Show per-subject stats |
|
||||
|
||||
@@ -32,6 +33,10 @@ instance (default: `https://phow2sim.sg.miti99.com`). Wraps two endpoints:
|
||||
so rounds stay guessable for casual players.
|
||||
- `GET /similarity?a=…&b=…` — cosine similarity + canonical forms +
|
||||
`in_vocab_a` / `in_vocab_b` flags.
|
||||
- `GET /neighbors?word=…&topn=100` — top-N nearest words (used by
|
||||
`/doantu_hint`). Results are filtered locally to skip capitalized
|
||||
foreign place names that leak in from the corpus, then 3 are sampled
|
||||
from the "warm but not hot" tail (skip the top 20%).
|
||||
|
||||
Override the base URL for local dev via `PHOW2SIM_API_URL`.
|
||||
|
||||
|
||||
@@ -89,5 +89,17 @@ export function createClient(apiBase, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
||||
similarity(a, b) {
|
||||
return fetchJson(buildUrl(apiBase, "/similarity", { a, b }), timeoutMs);
|
||||
},
|
||||
/**
|
||||
* Top-N nearest neighbors by cosine similarity.
|
||||
* @param {string} word
|
||||
* @param {number} [topn]
|
||||
* @returns {Promise<{
|
||||
* word: string, canonical: string|null, in_vocab: boolean,
|
||||
* neighbors: { word: string, similarity: number }[]
|
||||
* }>}
|
||||
*/
|
||||
neighbors(word, topn = 100) {
|
||||
return fetchJson(buildUrl(apiBase, "/neighbors", { word, topn }), timeoutMs);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +142,74 @@ async function submitGuess(ctx, { db, client }, subject, game, arg) {
|
||||
return ctx.reply(body, { parse_mode: "HTML" });
|
||||
}
|
||||
|
||||
// Corpus leaks foreign place names (mixed-case Latin — "al-Qantara",
|
||||
// "Nam_Afrin") into neighbors. Keep only lowercase tokens that look
|
||||
// Vietnamese: either a diacritic (non-ASCII) or a compound marker (`_`).
|
||||
const PLAYABLE_WORD = /^[\p{Ll}\p{M}_]+$/u;
|
||||
|
||||
/** @param {string} word — true if it has a diacritic or underscore compound. */
|
||||
function looksVietnamese(word) {
|
||||
if (word.includes("_")) return true;
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
if (word.charCodeAt(i) > 0x7f) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function pickHintWords(target, neighbors, alreadyGuessed, count) {
|
||||
const guessedSet = new Set(alreadyGuessed);
|
||||
const playable = neighbors.filter(
|
||||
(n) =>
|
||||
PLAYABLE_WORD.test(n.word) &&
|
||||
looksVietnamese(n.word) &&
|
||||
!n.word.includes(target) &&
|
||||
!target.includes(n.word) &&
|
||||
!guessedSet.has(n.word),
|
||||
);
|
||||
// "Warm but not hot" — skip the top 20% so a hint doesn't hand the answer away.
|
||||
const skip = Math.min(Math.floor(playable.length * 0.2), 20);
|
||||
const pool = playable.slice(skip);
|
||||
if (pool.length === 0) return [];
|
||||
// Fisher-Yates-ish sample without replacement.
|
||||
const picks = [];
|
||||
const used = new Set();
|
||||
const want = Math.min(count, pool.length);
|
||||
while (picks.length < want) {
|
||||
const i = Math.floor(Math.random() * pool.length);
|
||||
if (used.has(i)) continue;
|
||||
used.add(i);
|
||||
picks.push(pool[i]);
|
||||
}
|
||||
return picks;
|
||||
}
|
||||
|
||||
export async function handleHint(ctx, { db, client }) {
|
||||
const subject = getSubject(ctx);
|
||||
if (subject == null) return ctx.reply("Cannot identify chat.");
|
||||
const game = await loadGame(db, subject);
|
||||
if (!game || game.solved) {
|
||||
return ctx.reply("No active round. Send <code>/doantu</code> to start one.", {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
res = await client.neighbors(game.target, 100);
|
||||
} catch (err) {
|
||||
logFail("neighbors", err);
|
||||
return ctx.reply(UPSTREAM_FAIL);
|
||||
}
|
||||
const alreadyGuessed = game.guesses.map((g) => g.canonical);
|
||||
const picks = pickHintWords(game.target, res?.neighbors ?? [], alreadyGuessed, 3);
|
||||
if (picks.length === 0) {
|
||||
return ctx.reply("🤷 No usable hints available for this round.");
|
||||
}
|
||||
const list = picks.map((p) => `• <code>${escapeHtml(p.word)}</code>`).join("\n");
|
||||
return ctx.reply(`💡 <b>Hints</b> — related words (not the answer):\n${list}`, {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleGiveup(ctx, { db }) {
|
||||
const subject = getSubject(ctx);
|
||||
if (subject == null) return ctx.reply("Cannot identify chat.");
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { createClient } from "./api-client.js";
|
||||
import { handleDoantu, handleGiveup, handleStats } from "./handlers.js";
|
||||
import { handleDoantu, handleGiveup, handleHint, handleStats } from "./handlers.js";
|
||||
|
||||
const DEFAULT_API_URL = "https://phow2sim.sg.miti99.com";
|
||||
|
||||
@@ -31,6 +31,12 @@ const doantuModule = {
|
||||
description: "Đoán từ — Vietnamese semantic word guessing (unlimited tries)",
|
||||
handler: (ctx) => handleDoantu(ctx, { db, client }),
|
||||
},
|
||||
{
|
||||
name: "doantu_hint",
|
||||
visibility: "public",
|
||||
description: "Reveal 3 related words (not the answer) to nudge your guessing",
|
||||
handler: (ctx) => handleHint(ctx, { db, client }),
|
||||
},
|
||||
{
|
||||
name: "doantu_giveup",
|
||||
visibility: "public",
|
||||
|
||||
Reference in New Issue
Block a user