mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-28 16:21:32 +00:00
feat(semantle): add word2vec guessing game module
Telegram commands /semantle, /semantle_new, /semantle_giveup, /semantle_stats. Round starts with /random pick from hosted word2sim; each guess scored via /similarity. Unlimited guesses; solve on case-insensitive exact match. New env var WORD2SIM_API_URL (wrangler.toml, .env.deploy). Includes module README and 90 unit tests covering api-client, state, format, render, and handlers.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { createStore } from "../../../src/db/create-store.js";
|
||||
import {
|
||||
clearGame,
|
||||
loadGame,
|
||||
loadStats,
|
||||
recordResult,
|
||||
saveGame,
|
||||
} from "../../../src/modules/semantle/state.js";
|
||||
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
|
||||
|
||||
describe("semantle/state", () => {
|
||||
let db;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createStore("semantle", { KV: makeFakeKv() });
|
||||
});
|
||||
|
||||
describe("saveGame / loadGame", () => {
|
||||
it("round-trips game state", async () => {
|
||||
const subject = 12345;
|
||||
const game = {
|
||||
target: "apple",
|
||||
startedAt: Date.now(),
|
||||
solved: false,
|
||||
guesses: [
|
||||
{ word: "orange", canonical: "orange", similarity: 0.45 },
|
||||
{ word: "banana", canonical: "banana", similarity: 0.32 },
|
||||
],
|
||||
};
|
||||
|
||||
await saveGame(db, subject, game);
|
||||
const loaded = await loadGame(db, subject);
|
||||
|
||||
expect(loaded).toEqual(game);
|
||||
expect(loaded.guesses.length).toBe(2);
|
||||
expect(loaded.target).toBe("apple");
|
||||
});
|
||||
|
||||
it("returns null for non-existent game", async () => {
|
||||
const loaded = await loadGame(db, 999);
|
||||
expect(loaded).toBeNull();
|
||||
});
|
||||
|
||||
it("overwrites previous game on second save", async () => {
|
||||
const subject = 1;
|
||||
await saveGame(db, subject, { target: "a", startedAt: null, solved: false, guesses: [] });
|
||||
await saveGame(db, subject, { target: "b", startedAt: 100, solved: true, guesses: [] });
|
||||
|
||||
const loaded = await loadGame(db, subject);
|
||||
expect(loaded.target).toBe("b");
|
||||
expect(loaded.solved).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves null startedAt", async () => {
|
||||
const subject = 1;
|
||||
const game = {
|
||||
target: "test",
|
||||
startedAt: null,
|
||||
solved: false,
|
||||
guesses: [],
|
||||
};
|
||||
await saveGame(db, subject, game);
|
||||
const loaded = await loadGame(db, subject);
|
||||
expect(loaded.startedAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearGame", () => {
|
||||
it("removes game entry", async () => {
|
||||
const subject = 1;
|
||||
await saveGame(db, subject, {
|
||||
target: "x",
|
||||
startedAt: null,
|
||||
solved: false,
|
||||
guesses: [],
|
||||
});
|
||||
await clearGame(db, subject);
|
||||
const loaded = await loadGame(db, subject);
|
||||
expect(loaded).toBeNull();
|
||||
});
|
||||
|
||||
it("is idempotent", async () => {
|
||||
const subject = 1;
|
||||
await clearGame(db, subject);
|
||||
await clearGame(db, subject);
|
||||
expect(await loadGame(db, subject)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadStats", () => {
|
||||
it("returns defaults when no stats exist", async () => {
|
||||
const stats = await loadStats(db, 999);
|
||||
expect(stats).toEqual({
|
||||
played: 0,
|
||||
solved: 0,
|
||||
totalGuesses: 0,
|
||||
bestGuessCount: null,
|
||||
lastResultAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("loads existing stats", async () => {
|
||||
const subject = 1;
|
||||
await db.putJSON("stats:1", {
|
||||
played: 5,
|
||||
solved: 3,
|
||||
totalGuesses: 12,
|
||||
bestGuessCount: 2,
|
||||
lastResultAt: 123456,
|
||||
});
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.played).toBe(5);
|
||||
expect(stats.solved).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recordResult", () => {
|
||||
it("increments played on every result", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: false, guessCount: 3 });
|
||||
let stats = await loadStats(db, subject);
|
||||
expect(stats.played).toBe(1);
|
||||
|
||||
await recordResult(db, subject, { solved: false, guessCount: 2 });
|
||||
stats = await loadStats(db, subject);
|
||||
expect(stats.played).toBe(2);
|
||||
});
|
||||
|
||||
it("increments totalGuesses with each result", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: false, guessCount: 3 });
|
||||
await recordResult(db, subject, { solved: true, guessCount: 5 });
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.totalGuesses).toBe(8);
|
||||
});
|
||||
|
||||
it("increments solved only when solved=true", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: false, guessCount: 2 });
|
||||
await recordResult(db, subject, { solved: true, guessCount: 3 });
|
||||
await recordResult(db, subject, { solved: false, guessCount: 1 });
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.solved).toBe(1);
|
||||
expect(stats.played).toBe(3);
|
||||
});
|
||||
|
||||
it("sets bestGuessCount to first solved result count", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: true, guessCount: 7 });
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.bestGuessCount).toBe(7);
|
||||
});
|
||||
|
||||
it("updates bestGuessCount only if new count is lower", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: true, guessCount: 5 });
|
||||
await recordResult(db, subject, { solved: true, guessCount: 3 });
|
||||
await recordResult(db, subject, { solved: true, guessCount: 4 });
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.bestGuessCount).toBe(3);
|
||||
});
|
||||
|
||||
it("does not update bestGuessCount when not solved", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: true, guessCount: 5 });
|
||||
await recordResult(db, subject, { solved: false, guessCount: 2 });
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.bestGuessCount).toBe(5);
|
||||
});
|
||||
|
||||
it("keeps bestGuessCount as null if only losses", async () => {
|
||||
const subject = 1;
|
||||
await recordResult(db, subject, { solved: false, guessCount: 10 });
|
||||
await recordResult(db, subject, { solved: false, guessCount: 5 });
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.bestGuessCount).toBeNull();
|
||||
});
|
||||
|
||||
it("records lastResultAt timestamp", async () => {
|
||||
const subject = 1;
|
||||
const before = Date.now();
|
||||
await recordResult(db, subject, { solved: true, guessCount: 1 });
|
||||
const after = Date.now();
|
||||
|
||||
const stats = await loadStats(db, subject);
|
||||
expect(stats.lastResultAt).toBeGreaterThanOrEqual(before);
|
||||
expect(stats.lastResultAt).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
it("returns updated stats", async () => {
|
||||
const subject = 1;
|
||||
const returned = await recordResult(db, subject, { solved: true, guessCount: 2 });
|
||||
|
||||
expect(returned.played).toBe(1);
|
||||
expect(returned.solved).toBe(1);
|
||||
expect(returned.bestGuessCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user