mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-28 02:21:16 +00:00
feat(loldle): add ability and splash champion-guessing modules
Closes deferred phases 04 + 05 of loldle-new-modes plan. - loldle-ability: 5 guesses, DDragon ability icon as photo. State pins slot (P/Q/W/E/R) so the same icon shows every turn. Abilities pulled from DDragon per-champion — same source loldle.net uses at runtime. - loldle-splash: 4 guesses, random skin splash as photo. Skin pool scraped from loldle.net bundle (var Ad=[…] — 172 champs × 1939 skins, non-chroma, matches their splash mode exactly). URLs from Riot DDragon CDN (no version segment, stable across patches). - fetch-ddragon-data.js: extended to write all four JSONs in one run. Shares a single DDragon per-champion fetch cycle (concurrency 10). - Credits loldle.net + Riot Games in all loldle-family READMEs. 19 new tests (503 total). Lint clean. register:dry reports 12 loldle_* commands with no conflicts.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStore } from "../../../src/db/create-store.js";
|
||||
import abilitiesData from "../../../src/modules/loldle-ability/abilities.json" with {
|
||||
type: "json",
|
||||
};
|
||||
import {
|
||||
handleAbility,
|
||||
handleGiveup,
|
||||
handleStats,
|
||||
} from "../../../src/modules/loldle-ability/handlers.js";
|
||||
import { loadStats } from "../../../src/modules/loldle-ability/state.js";
|
||||
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
|
||||
|
||||
function pinRandom(value) {
|
||||
// deterministic: force Math.random() to the floor of the first entry
|
||||
vi.spyOn(Math, "random").mockReturnValue(value);
|
||||
}
|
||||
|
||||
function makeCtx({ text = "", fromId = 1, chatType = "private", chatId = 1 } = {}) {
|
||||
const replies = [];
|
||||
const photos = [];
|
||||
return {
|
||||
replies,
|
||||
photos,
|
||||
ctx: {
|
||||
from: { id: fromId },
|
||||
chat: { id: chatId, type: chatType },
|
||||
message: { text },
|
||||
reply: async (body, opts) => {
|
||||
replies.push({ body, opts });
|
||||
},
|
||||
replyWithPhoto: async (url, opts) => {
|
||||
photos.push({ url, opts });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("loldle-ability handlers — happy path", () => {
|
||||
let db;
|
||||
beforeEach(() => {
|
||||
db = createStore("loldle-ability", { KV: makeFakeKv() });
|
||||
pinRandom(0); // picks abilitiesData[0] + abilities[0]
|
||||
});
|
||||
|
||||
it("no-arg sends a photo with DDragon icon URL and caption", async () => {
|
||||
const { ctx, photos } = makeCtx();
|
||||
await handleAbility(ctx, db);
|
||||
expect(photos).toHaveLength(1);
|
||||
expect(photos[0].url).toMatch(/^https:\/\/ddragon\.leagueoflegends\.com\//);
|
||||
expect(photos[0].opts.caption).toMatch(/0\/5 guesses so far/);
|
||||
});
|
||||
|
||||
it("correct guess wins and cites slot + ability name", async () => {
|
||||
const target = abilitiesData[0];
|
||||
const { ctx, replies } = makeCtx({ text: `/loldle_ability ${target.championName}` });
|
||||
await handleAbility(ctx, db);
|
||||
expect(replies[0].body).toContain("🎉 Got it!");
|
||||
expect(replies[0].body).toContain(target.championName);
|
||||
expect(replies[0].body).toMatch(/\([PQWER]\)/);
|
||||
const s = await loadStats(db, 1);
|
||||
expect(s).toMatchObject({ played: 1, wins: 1, streak: 1 });
|
||||
});
|
||||
|
||||
it("wrong guess does not end the round", async () => {
|
||||
const wrong = abilitiesData[1];
|
||||
const { ctx, replies } = makeCtx({ text: `/loldle_ability ${wrong.championName}` });
|
||||
await handleAbility(ctx, db);
|
||||
expect(replies[0].body).toContain("❌");
|
||||
const s = await loadStats(db, 1);
|
||||
expect(s.played).toBe(0);
|
||||
});
|
||||
|
||||
it("giveup records loss and names the ability", async () => {
|
||||
await handleAbility(makeCtx().ctx, db);
|
||||
const { ctx, replies } = makeCtx();
|
||||
await handleGiveup(ctx, db);
|
||||
expect(replies[0].body).toContain("🏳️");
|
||||
expect(replies[0].body).toMatch(/\([PQWER]\)/);
|
||||
const s = await loadStats(db, 1);
|
||||
expect(s).toMatchObject({ played: 1, wins: 0 });
|
||||
});
|
||||
|
||||
it("stats renders zero-state", async () => {
|
||||
const { ctx, replies } = makeCtx();
|
||||
await handleStats(ctx, db);
|
||||
expect(replies[0].body).toContain("Played: 0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findChampion } from "../../../src/modules/loldle-ability/lookup.js";
|
||||
|
||||
const pool = [
|
||||
{ championName: "Ahri", abilities: [{ slot: "Q", name: "Orb of Deception", icon: "x" }] },
|
||||
{ championName: "Akali", abilities: [{ slot: "R", name: "Perfect Execution", icon: "x" }] },
|
||||
{ championName: "Miss Fortune", abilities: [{ slot: "P", name: "Love Tap", icon: "x" }] },
|
||||
];
|
||||
|
||||
describe("findChampion (ability pool)", () => {
|
||||
it("matches case-insensitively", () => {
|
||||
expect(findChampion(pool, "ahri").championName).toBe("Ahri");
|
||||
});
|
||||
|
||||
it("normalizes punctuation and spaces", () => {
|
||||
expect(findChampion(pool, "MissFortune").championName).toBe("Miss Fortune");
|
||||
expect(findChampion(pool, "miss fortune").championName).toBe("Miss Fortune");
|
||||
});
|
||||
|
||||
it("unique prefix resolves", () => {
|
||||
expect(findChampion(pool, "mi").championName).toBe("Miss Fortune");
|
||||
expect(findChampion(pool, "ak").championName).toBe("Akali");
|
||||
});
|
||||
|
||||
it("returns null on empty / no-match / ambiguous", () => {
|
||||
expect(findChampion(pool, "")).toBeNull();
|
||||
expect(findChampion(pool, "zzz")).toBeNull();
|
||||
expect(findChampion(pool, "a")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { createStore } from "../../../src/db/create-store.js";
|
||||
import {
|
||||
clearGame,
|
||||
loadGame,
|
||||
loadStats,
|
||||
recordResult,
|
||||
saveGame,
|
||||
} from "../../../src/modules/loldle-ability/state.js";
|
||||
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
|
||||
|
||||
describe("loldle-ability state", () => {
|
||||
let db;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createStore("loldle-ability", { KV: makeFakeKv() });
|
||||
});
|
||||
|
||||
it("round-trips a game with slot field", async () => {
|
||||
const state = { target: "Ahri", slot: "Q", guesses: ["Akali"], startedAt: 1234 };
|
||||
await saveGame(db, 42, state);
|
||||
expect(await loadGame(db, 42)).toEqual(state);
|
||||
});
|
||||
|
||||
it("clearGame removes the record", async () => {
|
||||
await saveGame(db, 42, { target: "Ahri", slot: "P", guesses: [], startedAt: null });
|
||||
await clearGame(db, 42);
|
||||
expect(await loadGame(db, 42)).toBeNull();
|
||||
});
|
||||
|
||||
it("recordResult tracks streaks + bestStreak", async () => {
|
||||
await recordResult(db, 42, true);
|
||||
await recordResult(db, 42, true);
|
||||
await recordResult(db, 42, false);
|
||||
const s = await loadStats(db, 42);
|
||||
expect(s).toMatchObject({ played: 3, wins: 2, streak: 0, bestStreak: 2 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStore } from "../../../src/db/create-store.js";
|
||||
import {
|
||||
handleGiveup,
|
||||
handleSplash,
|
||||
handleStats,
|
||||
} from "../../../src/modules/loldle-splash/handlers.js";
|
||||
import splashesData from "../../../src/modules/loldle-splash/splashes.json" with { type: "json" };
|
||||
import { loadStats } from "../../../src/modules/loldle-splash/state.js";
|
||||
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
|
||||
|
||||
function pinRandom(value) {
|
||||
vi.spyOn(Math, "random").mockReturnValue(value);
|
||||
}
|
||||
|
||||
function makeCtx({ text = "", fromId = 1, chatType = "private", chatId = 1 } = {}) {
|
||||
const replies = [];
|
||||
const photos = [];
|
||||
return {
|
||||
replies,
|
||||
photos,
|
||||
ctx: {
|
||||
from: { id: fromId },
|
||||
chat: { id: chatId, type: chatType },
|
||||
message: { text },
|
||||
reply: async (body, opts) => {
|
||||
replies.push({ body, opts });
|
||||
},
|
||||
replyWithPhoto: async (url, opts) => {
|
||||
photos.push({ url, opts });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("loldle-splash handlers — happy path", () => {
|
||||
let db;
|
||||
beforeEach(() => {
|
||||
db = createStore("loldle-splash", { KV: makeFakeKv() });
|
||||
pinRandom(0); // picks first champion + first skin
|
||||
});
|
||||
|
||||
it("no-arg sends a splash photo with 0/4 caption", async () => {
|
||||
const { ctx, photos } = makeCtx();
|
||||
await handleSplash(ctx, db);
|
||||
expect(photos).toHaveLength(1);
|
||||
expect(photos[0].url).toMatch(
|
||||
/^https:\/\/ddragon\.leagueoflegends\.com\/cdn\/img\/champion\/splash\//,
|
||||
);
|
||||
expect(photos[0].opts.caption).toMatch(/0\/4 guesses so far/);
|
||||
});
|
||||
|
||||
it("correct guess wins and names the skin", async () => {
|
||||
const target = splashesData[0];
|
||||
const { ctx, replies } = makeCtx({ text: `/loldle_splash ${target.championName}` });
|
||||
await handleSplash(ctx, db);
|
||||
expect(replies[0].body).toContain("🎉 Got it!");
|
||||
expect(replies[0].body).toContain("skin");
|
||||
const s = await loadStats(db, 1);
|
||||
expect(s).toMatchObject({ played: 1, wins: 1 });
|
||||
});
|
||||
|
||||
it("giveup records loss and names skin", async () => {
|
||||
await handleSplash(makeCtx().ctx, db);
|
||||
const { ctx, replies } = makeCtx();
|
||||
await handleGiveup(ctx, db);
|
||||
expect(replies[0].body).toContain("🏳️");
|
||||
expect(replies[0].body).toContain("skin");
|
||||
});
|
||||
|
||||
it("stats renders zero-state", async () => {
|
||||
const { ctx, replies } = makeCtx();
|
||||
await handleStats(ctx, db);
|
||||
expect(replies[0].body).toContain("Played: 0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { createStore } from "../../../src/db/create-store.js";
|
||||
import {
|
||||
clearGame,
|
||||
loadGame,
|
||||
loadStats,
|
||||
recordResult,
|
||||
saveGame,
|
||||
} from "../../../src/modules/loldle-splash/state.js";
|
||||
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
|
||||
|
||||
describe("loldle-splash state", () => {
|
||||
let db;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createStore("loldle-splash", { KV: makeFakeKv() });
|
||||
});
|
||||
|
||||
it("round-trips a game with skinId field", async () => {
|
||||
const state = { target: "Ahri", skinId: 9, guesses: [], startedAt: null };
|
||||
await saveGame(db, 99, state);
|
||||
expect(await loadGame(db, 99)).toEqual(state);
|
||||
});
|
||||
|
||||
it("clearGame removes the record", async () => {
|
||||
await saveGame(db, 99, { target: "Ahri", skinId: 0, guesses: [], startedAt: null });
|
||||
await clearGame(db, 99);
|
||||
expect(await loadGame(db, 99)).toBeNull();
|
||||
});
|
||||
|
||||
it("recordResult tracks streaks + bestStreak", async () => {
|
||||
await recordResult(db, 99, true);
|
||||
await recordResult(db, 99, true);
|
||||
const s = await loadStats(db, 99);
|
||||
expect(s).toMatchObject({ played: 2, wins: 2, streak: 2, bestStreak: 2 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user