@zhin.js/plugin-word-riddle 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +20 -0
  2. package/lib/commands.d.ts +6 -0
  3. package/lib/commands.d.ts.map +1 -0
  4. package/lib/commands.js +94 -0
  5. package/lib/commands.js.map +1 -0
  6. package/lib/data/char-riddles.json +1 -0
  7. package/lib/data/riddles.d.ts +3 -0
  8. package/lib/data/riddles.d.ts.map +1 -0
  9. package/lib/data/riddles.js +2 -0
  10. package/lib/data/riddles.js.map +1 -0
  11. package/lib/engine.d.ts +9 -0
  12. package/lib/engine.d.ts.map +1 -0
  13. package/lib/engine.js +20 -0
  14. package/lib/engine.js.map +1 -0
  15. package/lib/game-flow.d.ts +12 -0
  16. package/lib/game-flow.d.ts.map +1 -0
  17. package/lib/game-flow.js +141 -0
  18. package/lib/game-flow.js.map +1 -0
  19. package/lib/hub-register.d.ts +5 -0
  20. package/lib/hub-register.d.ts.map +1 -0
  21. package/lib/hub-register.js +32 -0
  22. package/lib/hub-register.js.map +1 -0
  23. package/lib/index.d.ts +2 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +26 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/models.d.ts +31 -0
  28. package/lib/models.d.ts.map +1 -0
  29. package/lib/models.js +25 -0
  30. package/lib/models.js.map +1 -0
  31. package/lib/riddle-command.d.ts +5 -0
  32. package/lib/riddle-command.d.ts.map +1 -0
  33. package/lib/riddle-command.js +56 -0
  34. package/lib/riddle-command.js.map +1 -0
  35. package/lib/riddle-provider.d.ts +21 -0
  36. package/lib/riddle-provider.d.ts.map +1 -0
  37. package/lib/riddle-provider.js +80 -0
  38. package/lib/riddle-provider.js.map +1 -0
  39. package/lib/session-service.d.ts +20 -0
  40. package/lib/session-service.d.ts.map +1 -0
  41. package/lib/session-service.js +108 -0
  42. package/lib/session-service.js.map +1 -0
  43. package/lib/view.d.ts +6 -0
  44. package/lib/view.d.ts.map +1 -0
  45. package/lib/view.js +50 -0
  46. package/lib/view.js.map +1 -0
  47. package/package.json +39 -0
  48. package/plugin.yml +2 -0
  49. package/src/commands.ts +112 -0
  50. package/src/data/char-riddles.json +1 -0
  51. package/src/data/riddles.ts +8 -0
  52. package/src/engine.ts +30 -0
  53. package/src/game-flow.ts +196 -0
  54. package/src/hub-register.ts +34 -0
  55. package/src/index.ts +32 -0
  56. package/src/models.ts +57 -0
  57. package/src/riddle-command.ts +66 -0
  58. package/src/riddle-provider.ts +105 -0
  59. package/src/session-service.ts +113 -0
  60. package/src/view.ts +60 -0
@@ -0,0 +1,108 @@
1
+ import { channelKey, generateSessionId } from '@zhin.js/game-shared';
2
+ import { pickRoundQueue } from './data/riddles.js';
3
+ function getModel(db) {
4
+ const model = db.models.get('word_riddle_sessions');
5
+ if (!model)
6
+ throw new Error('word_riddle_sessions not registered');
7
+ return model;
8
+ }
9
+ export function parseQueue(value) {
10
+ if (Array.isArray(value))
11
+ return value.filter((x) => typeof x === 'string');
12
+ if (typeof value !== 'string' || !value)
13
+ return [];
14
+ try {
15
+ const v = JSON.parse(value);
16
+ return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ }
22
+ export class SessionService {
23
+ db;
24
+ constructor(db) {
25
+ this.db = db;
26
+ }
27
+ async getActiveByChannel(channel) {
28
+ const rows = await getModel(this.db).findAll({ channel_key: channel, status: 'active' });
29
+ return rows[0] ?? null;
30
+ }
31
+ async getActiveForUser(channel, userId) {
32
+ const row = await this.getActiveByChannel(channel);
33
+ if (!row || row.player_id !== userId)
34
+ return null;
35
+ return row;
36
+ }
37
+ async getById(id) {
38
+ return getModel(this.db).findOne({ id });
39
+ }
40
+ async getActiveByBoardMessageId(messageId) {
41
+ if (!messageId)
42
+ return null;
43
+ const rows = await getModel(this.db).findAll({ status: 'active' });
44
+ for (const row of rows) {
45
+ const stored = row.board_message_id;
46
+ if (!stored)
47
+ continue;
48
+ if (stored === messageId || stored.endsWith(`:${messageId}`))
49
+ return row;
50
+ }
51
+ return null;
52
+ }
53
+ async createSession(message, mode) {
54
+ const now = Date.now();
55
+ const queue = pickRoundQueue(mode).map((r) => r.id);
56
+ const row = {
57
+ id: generateSessionId(),
58
+ adapter: String(message.$adapter),
59
+ endpoint: message.$endpoint,
60
+ channel_type: message.$channel.type,
61
+ channel_id: message.$channel.id,
62
+ channel_key: channelKey(message),
63
+ player_id: message.$sender.id,
64
+ player_name: message.$sender.name?.trim() || message.$sender.id,
65
+ mode,
66
+ queue: JSON.stringify(queue),
67
+ index: 0,
68
+ score: 0,
69
+ streak: 0,
70
+ best_streak: 0,
71
+ hints_used: 0,
72
+ wrong_count: 0,
73
+ status: 'active',
74
+ board_message_id: '',
75
+ updated_at: now,
76
+ created_at: now,
77
+ };
78
+ await getModel(this.db).create(row);
79
+ return row;
80
+ }
81
+ async updateSession(id, patch) {
82
+ await getModel(this.db).updateWhere({ id }, { ...patch, updated_at: Date.now() });
83
+ }
84
+ async abortStale(idleMs) {
85
+ const cutoff = Date.now() - idleMs;
86
+ const model = getModel(this.db);
87
+ const rows = await model.findAll({ status: 'active' });
88
+ let n = 0;
89
+ for (const row of rows) {
90
+ if (row.updated_at < cutoff) {
91
+ await model.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
92
+ n++;
93
+ }
94
+ }
95
+ return n;
96
+ }
97
+ }
98
+ export function createServices(db) {
99
+ return new SessionService(db);
100
+ }
101
+ export function resolveGameDatabase(feature) {
102
+ return feature.db;
103
+ }
104
+ export function currentRiddleId(session) {
105
+ const queue = parseQueue(session.queue);
106
+ return queue[session.index] ?? null;
107
+ }
108
+ //# sourceMappingURL=session-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-service.js","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"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"}
package/lib/view.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { SendContent } from 'zhin.js';
2
+ import type { RiddleSessionRow } from './models.js';
3
+ declare const MAX_WRONG = 3;
4
+ export declare function buildRiddleView(session: RiddleSessionRow, eventLines?: string[]): SendContent | string;
5
+ export { MAX_WRONG };
6
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../src/view.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,QAAA,MAAM,SAAS,IAAI,CAAC;AAEpB,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EACzB,UAAU,GAAE,MAAM,EAAO,GACxB,WAAW,GAAG,MAAM,CA8CtB;AAED,OAAO,EAAE,SAAS,EAAE,CAAC"}
package/lib/view.js ADDED
@@ -0,0 +1,50 @@
1
+ import { buildChoiceKeyboard } from '@zhin.js/game-shared';
2
+ import { getRiddleById, RIDDLE_PREFIX, typeLabel } from './engine.js';
3
+ import { currentRiddleId, parseQueue } from './session-service.js';
4
+ const MAX_WRONG = 3;
5
+ export function buildRiddleView(session, eventLines = []) {
6
+ const queue = parseQueue(session.queue);
7
+ const terminal = session.status !== 'active';
8
+ const riddleId = currentRiddleId(session);
9
+ const entry = riddleId ? getRiddleById(riddleId) : undefined;
10
+ const lines = [
11
+ `🧩 **${typeLabel(session.mode)}**`,
12
+ '',
13
+ `进度 ${Math.min(session.index + 1, queue.length)}/${queue.length} · 得分 **${session.score}** · 连击 **${session.streak}**(最佳 ${session.best_streak})`,
14
+ ];
15
+ if (terminal) {
16
+ lines.push('', '🎊 **本轮题目已全部完成!**', `最终得分:**${session.score}**`);
17
+ }
18
+ else if (entry) {
19
+ lines.push('', `❓ ${entry.question}`);
20
+ if (session.wrong_count > 0) {
21
+ lines.push(`⚠️ 本题失误 ${session.wrong_count}/${MAX_WRONG}`);
22
+ }
23
+ lines.push('', session.mode === 'char' ? '请回复一个汉字。' : '请回复四字成语。');
24
+ }
25
+ if (eventLines.length)
26
+ lines.push('', ...eventLines);
27
+ if (!entry && !terminal)
28
+ return '题目加载失败,请重新开始。';
29
+ const choices = terminal
30
+ ? [
31
+ { id: 'restart_char', label: '🔤 再来字谜', style: 'primary' },
32
+ { id: 'restart_idiom', label: '📜 再来成语', style: 'primary' },
33
+ ]
34
+ : [
35
+ { id: 'hint', label: '💡 提示', style: 'secondary' },
36
+ { id: 'skip', label: '⏭️ 跳过', style: 'secondary' },
37
+ { id: 'quit', label: '🏳️ 结束', style: 'danger' },
38
+ ];
39
+ return buildChoiceKeyboard({
40
+ gamePrefix: RIDDLE_PREFIX,
41
+ sessionId: session.id,
42
+ narrative: lines.join('\n'),
43
+ choices,
44
+ terminal,
45
+ buttonsPerRow: terminal ? 2 : 3,
46
+ fallbackHint: '回复答案,或 1提示 2跳过 3结束',
47
+ });
48
+ }
49
+ export { MAX_WRONG };
50
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../src/view.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@zhin.js/plugin-word-riddle",
3
+ "version": "1.0.0",
4
+ "description": "Zhin.js 猜谜 — 字谜与猜成语",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "development": "./src/index.ts",
12
+ "import": "./lib/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "lib",
19
+ "plugin.yml",
20
+ "README.md"
21
+ ],
22
+ "license": "MIT",
23
+ "peerDependencies": {
24
+ "zhin.js": "4.1.0"
25
+ },
26
+ "dependencies": {
27
+ "chinese-idiom-chengyu": "^1.1.0",
28
+ "@zhin.js/game-shared": "1.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^6.0.3",
32
+ "zhin.js": "4.1.0"
33
+ },
34
+ "scripts": {
35
+ "build:data": "node scripts/build-char-riddles.mjs",
36
+ "build": "pnpm run build:data && tsc --build && node -e \"import('node:fs/promises').then(fs=>fs.mkdir('lib/data',{recursive:true}).then(()=>fs.copyFile('src/data/char-riddles.json','lib/data/char-riddles.json')))\"",
37
+ "clean": "rimraf lib"
38
+ }
39
+ }
package/plugin.yml ADDED
@@ -0,0 +1,2 @@
1
+ name: "@zhin.js/plugin-word-riddle"
2
+ description: "字谜与猜成语"
@@ -0,0 +1,112 @@
1
+ import { Message, MessageCommand, getActionFromMessage, type Plugin } from 'zhin.js';
2
+ import {
3
+ buildChoiceFallbackMap,
4
+ channelKey,
5
+ normalizeRiddleAction,
6
+ parseChoicePayload,
7
+ registerGameTextMiddleware,
8
+ } from '@zhin.js/game-shared';
9
+ import { handleChoice, processAnswerText, RIDDLE_PREFIX } from './game-flow.js';
10
+ import { runRiddleCommand } from './riddle-command.js';
11
+ import type { SessionService } from './session-service.js';
12
+
13
+ function registerPattern(
14
+ plugin: Plugin,
15
+ pattern: string,
16
+ desc: string,
17
+ getServices: () => SessionService | null,
18
+ ): void {
19
+ plugin.addCommand(
20
+ new MessageCommand(pattern)
21
+ .desc(desc)
22
+ .action(async (message, result) => {
23
+ const services = getServices();
24
+ if (!services) return '猜谜需要启用 database 配置。';
25
+ const raw = (result.params.action as string | undefined) ?? '';
26
+ return runRiddleCommand(plugin, services, message, normalizeRiddleAction(raw));
27
+ }),
28
+ );
29
+ }
30
+
31
+ export function registerCommands(plugin: Plugin, getServices: () => SessionService | null): void {
32
+ registerPattern(plugin, 'riddle [action:word]', '猜谜(riddle)', getServices);
33
+ registerPattern(plugin, '猜谜 [action:word]', '猜谜(中文)', getServices);
34
+ }
35
+
36
+ async function resolveChoice(
37
+ message: Message<any>,
38
+ services: SessionService,
39
+ ): Promise<{ sessionId: string; choiceId: string } | null> {
40
+ const action = getActionFromMessage(message);
41
+ if (!action) return null;
42
+
43
+ const parsed = parseChoicePayload(action.payload, RIDDLE_PREFIX);
44
+ if (parsed) return { sessionId: parsed.sessionId, choiceId: parsed.choiceId };
45
+
46
+ const choiceId = action.id || action.payload;
47
+ if (!choiceId) return null;
48
+
49
+ const ch = channelKey(message);
50
+ const session =
51
+ await services.getActiveForUser(ch, message.$sender.id)
52
+ ?? (action.sourceMessageId
53
+ ? await services.getActiveByBoardMessageId(action.sourceMessageId)
54
+ : null);
55
+ if (!session || session.channel_key !== ch) return null;
56
+
57
+ const valid = ['hint', 'skip', 'quit', 'restart_char', 'restart_idiom'];
58
+ if (!valid.includes(choiceId)) return null;
59
+ return { sessionId: session.id, choiceId };
60
+ }
61
+
62
+ export function registerInteractive(plugin: Plugin, getServices: () => SessionService | null): void {
63
+ plugin.registerInteractiveHandler(`${RIDDLE_PREFIX}:`, async (message) => {
64
+ const services = getServices();
65
+ if (!services) return false;
66
+ const choice = await resolveChoice(message, services);
67
+ if (!choice) return false;
68
+ const err = await handleChoice(plugin, services, message, choice.sessionId, choice.choiceId);
69
+ if (typeof err === 'string') await message.$reply?.(err);
70
+ return true;
71
+ });
72
+ }
73
+
74
+ export function registerTextMiddleware(plugin: Plugin, getServices: () => SessionService | null): void {
75
+ registerGameTextMiddleware(plugin, async (message, next) => {
76
+ const services = getServices();
77
+ if (!services) return next();
78
+
79
+ const action = getActionFromMessage(message);
80
+ if (action?.payload.startsWith(`${RIDDLE_PREFIX}:`)) return next();
81
+
82
+ const ch = channelKey(message);
83
+ const session = await services.getActiveForUser(ch, message.$sender.id);
84
+ if (!session) return next();
85
+
86
+ const raw = message.$raw?.trim() ?? '';
87
+ if (!raw) return next();
88
+
89
+ const n = /^(\d+)$/.exec(raw);
90
+ if (n && session.status === 'active') {
91
+ const map = buildChoiceFallbackMap(RIDDLE_PREFIX, session.id, [
92
+ { id: 'hint', label: '提示' },
93
+ { id: 'skip', label: '跳过' },
94
+ { id: 'quit', label: '结束' },
95
+ ]);
96
+ const payload = map[n[1]!];
97
+ const parsed = payload ? parseChoicePayload(payload, RIDDLE_PREFIX) : null;
98
+ if (parsed?.sessionId === session.id) {
99
+ const err = await handleChoice(plugin, services, message, session.id, parsed.choiceId);
100
+ if (typeof err === 'string') await message.$reply?.(err);
101
+ return;
102
+ }
103
+ }
104
+
105
+ if (/^[\u4e00-\u9fff]{1,8}$/.test(raw)) {
106
+ await processAnswerText(plugin, services, message, raw);
107
+ return;
108
+ }
109
+
110
+ return next();
111
+ }, 'word-riddle:text');
112
+ }