@zhin.js/plugin-idiom-chain 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.
- package/lib/chain-command.d.ts +5 -0
- package/lib/chain-command.d.ts.map +1 -0
- package/lib/chain-command.js +48 -0
- package/lib/chain-command.js.map +1 -0
- package/lib/commands.d.ts +6 -0
- package/lib/commands.d.ts.map +1 -0
- package/lib/commands.js +94 -0
- package/lib/commands.js.map +1 -0
- package/lib/data/idioms.d.ts +8 -0
- package/lib/data/idioms.d.ts.map +1 -0
- package/lib/data/idioms.js +422 -0
- package/lib/data/idioms.js.map +1 -0
- package/lib/engine.d.ts +4 -0
- package/lib/engine.d.ts.map +1 -0
- package/lib/engine.js +3 -0
- package/lib/engine.js.map +1 -0
- package/lib/game-flow.d.ts +11 -0
- package/lib/game-flow.d.ts.map +1 -0
- package/lib/game-flow.js +203 -0
- package/lib/game-flow.js.map +1 -0
- package/lib/hub-register.d.ts +5 -0
- package/lib/hub-register.d.ts.map +1 -0
- package/lib/hub-register.js +31 -0
- package/lib/hub-register.js.map +1 -0
- package/lib/idiom-provider.d.ts +23 -0
- package/lib/idiom-provider.d.ts.map +1 -0
- package/lib/idiom-provider.js +110 -0
- package/lib/idiom-provider.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +26 -0
- package/lib/index.js.map +1 -0
- package/lib/models.d.ts +35 -0
- package/lib/models.d.ts.map +1 -0
- package/lib/models.js +28 -0
- package/lib/models.js.map +1 -0
- package/lib/session-service.d.ts +25 -0
- package/lib/session-service.d.ts.map +1 -0
- package/lib/session-service.js +108 -0
- package/lib/session-service.js.map +1 -0
- package/lib/view.d.ts +6 -0
- package/lib/view.d.ts.map +1 -0
- package/lib/view.js +54 -0
- package/lib/view.js.map +1 -0
- package/package.json +38 -0
- package/plugin.yml +2 -0
- package/src/chain-command.ts +57 -0
- package/src/commands.ts +112 -0
- package/src/engine.ts +18 -0
- package/src/game-flow.ts +270 -0
- package/src/hub-register.ts +32 -0
- package/src/idiom-provider.ts +165 -0
- package/src/index.ts +32 -0
- package/src/models.ts +64 -0
- package/src/session-service.ts +117 -0
- package/src/view.ts +65 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Database, DatabaseFeature, Message, Models } from 'zhin.js';
|
|
2
|
+
import type { MatchMode } from './engine.js';
|
|
3
|
+
import type { ChainSessionRow } from './models.js';
|
|
4
|
+
export type ChainDatabase = Database<unknown, Models, string>;
|
|
5
|
+
export declare function parseUsed(value: string | string[] | unknown): Set<string>;
|
|
6
|
+
export declare function serializeUsed(set: Set<string>): string;
|
|
7
|
+
export declare class SessionService {
|
|
8
|
+
private readonly db;
|
|
9
|
+
constructor(db: ChainDatabase);
|
|
10
|
+
getActiveByChannel(channel: string): Promise<ChainSessionRow | null>;
|
|
11
|
+
getActiveForUser(channel: string, userId: string): Promise<ChainSessionRow | null>;
|
|
12
|
+
getById(id: string): Promise<ChainSessionRow | null>;
|
|
13
|
+
getActiveByBoardMessageId(messageId: string): Promise<ChainSessionRow | null>;
|
|
14
|
+
createSession(message: Message<any>, starter: {
|
|
15
|
+
text: string;
|
|
16
|
+
nextChar: string;
|
|
17
|
+
used: string[];
|
|
18
|
+
matchMode: MatchMode;
|
|
19
|
+
}): Promise<ChainSessionRow>;
|
|
20
|
+
updateSession(id: string, patch: Partial<ChainSessionRow>): Promise<void>;
|
|
21
|
+
abortStale(idleMs: number): Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createServices(db: ChainDatabase): SessionService;
|
|
24
|
+
export declare function resolveGameDatabase(feature: DatabaseFeature): ChainDatabase;
|
|
25
|
+
//# sourceMappingURL=session-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-service.d.ts","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAgB,MAAM,SAAS,CAAC;AAExF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAQ9D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CASzE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAEtD;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,aAAa;IAExC,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAKpE,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAMlF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAIpD,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAW7E,aAAa,CACjB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,SAAS,CAAA;KAAE,GAChF,OAAO,CAAC,eAAe,CAAC;IA+BrB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzE,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAalD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,aAAa,GAAG,cAAc,CAEhE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,GAAG,aAAa,CAE3E"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { channelKey, generateSessionId } from '@zhin.js/game-shared';
|
|
2
|
+
function getModel(db) {
|
|
3
|
+
const model = db.models.get('idiom_chain_sessions');
|
|
4
|
+
if (!model)
|
|
5
|
+
throw new Error('idiom_chain_sessions not registered');
|
|
6
|
+
return model;
|
|
7
|
+
}
|
|
8
|
+
export function parseUsed(value) {
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return new Set(value.filter((x) => typeof x === 'string'));
|
|
11
|
+
if (typeof value !== 'string' || !value)
|
|
12
|
+
return new Set();
|
|
13
|
+
try {
|
|
14
|
+
const v = JSON.parse(value);
|
|
15
|
+
return Array.isArray(v) ? new Set(v.filter((x) => typeof x === 'string')) : new Set();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return new Set();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function serializeUsed(set) {
|
|
22
|
+
return JSON.stringify([...set]);
|
|
23
|
+
}
|
|
24
|
+
export class SessionService {
|
|
25
|
+
db;
|
|
26
|
+
constructor(db) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
}
|
|
29
|
+
async getActiveByChannel(channel) {
|
|
30
|
+
const rows = await getModel(this.db).findAll({ channel_key: channel, status: 'active' });
|
|
31
|
+
return rows[0] ?? null;
|
|
32
|
+
}
|
|
33
|
+
async getActiveForUser(channel, userId) {
|
|
34
|
+
const row = await this.getActiveByChannel(channel);
|
|
35
|
+
if (!row || row.player_id !== userId)
|
|
36
|
+
return null;
|
|
37
|
+
return row;
|
|
38
|
+
}
|
|
39
|
+
async getById(id) {
|
|
40
|
+
return getModel(this.db).findOne({ id });
|
|
41
|
+
}
|
|
42
|
+
async getActiveByBoardMessageId(messageId) {
|
|
43
|
+
if (!messageId)
|
|
44
|
+
return null;
|
|
45
|
+
const rows = await getModel(this.db).findAll({ status: 'active' });
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const stored = row.board_message_id;
|
|
48
|
+
if (!stored)
|
|
49
|
+
continue;
|
|
50
|
+
if (stored === messageId || stored.endsWith(`:${messageId}`))
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
async createSession(message, starter) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const row = {
|
|
58
|
+
id: generateSessionId(),
|
|
59
|
+
adapter: String(message.$adapter),
|
|
60
|
+
endpoint: message.$endpoint,
|
|
61
|
+
channel_type: message.$channel.type,
|
|
62
|
+
channel_id: message.$channel.id,
|
|
63
|
+
channel_key: channelKey(message),
|
|
64
|
+
player_id: message.$sender.id,
|
|
65
|
+
player_name: message.$sender.name?.trim() || message.$sender.id,
|
|
66
|
+
last_idiom: starter.text,
|
|
67
|
+
next_char: starter.nextChar,
|
|
68
|
+
match_mode: starter.matchMode,
|
|
69
|
+
used_idioms: serializeUsed(new Set(starter.used)),
|
|
70
|
+
player_score: 0,
|
|
71
|
+
bot_score: 0,
|
|
72
|
+
streak: 0,
|
|
73
|
+
best_streak: 0,
|
|
74
|
+
wrong_count: 0,
|
|
75
|
+
hints_used: 0,
|
|
76
|
+
turn: 'player',
|
|
77
|
+
status: 'active',
|
|
78
|
+
board_message_id: '',
|
|
79
|
+
updated_at: now,
|
|
80
|
+
created_at: now,
|
|
81
|
+
};
|
|
82
|
+
await getModel(this.db).create(row);
|
|
83
|
+
return row;
|
|
84
|
+
}
|
|
85
|
+
async updateSession(id, patch) {
|
|
86
|
+
await getModel(this.db).updateWhere({ id }, { ...patch, updated_at: Date.now() });
|
|
87
|
+
}
|
|
88
|
+
async abortStale(idleMs) {
|
|
89
|
+
const cutoff = Date.now() - idleMs;
|
|
90
|
+
const model = getModel(this.db);
|
|
91
|
+
const rows = await model.findAll({ status: 'active' });
|
|
92
|
+
let n = 0;
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
if (row.updated_at < cutoff) {
|
|
95
|
+
await model.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
|
|
96
|
+
n++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return n;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function createServices(db) {
|
|
103
|
+
return new SessionService(db);
|
|
104
|
+
}
|
|
105
|
+
export function resolveGameDatabase(feature) {
|
|
106
|
+
return feature.db;
|
|
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 { ChainSessionRow } from './models.js';
|
|
3
|
+
declare const MAX_WRONG = 3;
|
|
4
|
+
export declare function buildChainView(session: ChainSessionRow, eventLines?: string[]): SendContent;
|
|
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;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,QAAA,MAAM,SAAS,IAAI,CAAC;AAMpB,wBAAgB,cAAc,CAC5B,OAAO,EAAE,eAAe,EACxB,UAAU,GAAE,MAAM,EAAO,GACxB,WAAW,CAgDb;AAED,OAAO,EAAE,SAAS,EAAE,CAAC"}
|
package/lib/view.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildChoiceKeyboard } from '@zhin.js/game-shared';
|
|
2
|
+
import { CHAIN_PREFIX, getGloss, idiomCount, modeLabel, promptLine } from './engine.js';
|
|
3
|
+
const MAX_WRONG = 3;
|
|
4
|
+
function sessionMode(session) {
|
|
5
|
+
return session.match_mode === 'char' ? 'char' : 'pinyin';
|
|
6
|
+
}
|
|
7
|
+
export function buildChainView(session, eventLines = []) {
|
|
8
|
+
const terminal = session.status !== 'active';
|
|
9
|
+
const mode = sessionMode(session);
|
|
10
|
+
const lines = [
|
|
11
|
+
`📜 **成语接龙** · ${modeLabel(mode)}`,
|
|
12
|
+
'',
|
|
13
|
+
`词库 **${idiomCount()}** 条 · 局分 ${session.player_score}:${session.bot_score} · 连击 **${session.streak}**(最佳 ${session.best_streak})`,
|
|
14
|
+
];
|
|
15
|
+
if (session.last_idiom) {
|
|
16
|
+
const gloss = getGloss(session.last_idiom);
|
|
17
|
+
lines.push('', `上一句:**${session.last_idiom}**${gloss ? `(${gloss})` : ''}`);
|
|
18
|
+
lines.push(promptLine(session.last_idiom, mode));
|
|
19
|
+
}
|
|
20
|
+
if (session.wrong_count > 0 && session.status === 'active') {
|
|
21
|
+
lines.push(`⚠️ 失误 ${session.wrong_count}/${MAX_WRONG}`);
|
|
22
|
+
}
|
|
23
|
+
if (eventLines.length) {
|
|
24
|
+
lines.push('', ...eventLines);
|
|
25
|
+
}
|
|
26
|
+
if (terminal) {
|
|
27
|
+
lines.push('');
|
|
28
|
+
if (session.status === 'won')
|
|
29
|
+
lines.push('🏆 **你赢了本局!** 机器人接不上啦。');
|
|
30
|
+
else if (session.status === 'lost')
|
|
31
|
+
lines.push('💀 **机器人获胜。** 发送「接龙 开始」再来。');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
lines.push('', '直接回复四字成语,或点下方按钮:');
|
|
35
|
+
}
|
|
36
|
+
const choices = terminal
|
|
37
|
+
? [{ id: 'restart', label: '🔄 再来一局', style: 'primary' }]
|
|
38
|
+
: [
|
|
39
|
+
{ id: 'hint', label: '💡 提示', style: 'secondary' },
|
|
40
|
+
{ id: 'skip', label: '⏭️ 跳过', style: 'secondary' },
|
|
41
|
+
{ id: 'quit', label: '🏳️ 认输', style: 'danger' },
|
|
42
|
+
];
|
|
43
|
+
return buildChoiceKeyboard({
|
|
44
|
+
gamePrefix: CHAIN_PREFIX,
|
|
45
|
+
sessionId: session.id,
|
|
46
|
+
narrative: lines.join('\n'),
|
|
47
|
+
choices,
|
|
48
|
+
terminal,
|
|
49
|
+
buttonsPerRow: 3,
|
|
50
|
+
fallbackHint: '回复成语,或 1提示 2跳过 3认输',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export { MAX_WRONG };
|
|
54
|
+
//# sourceMappingURL=view.js.map
|
package/lib/view.js.map
ADDED
|
@@ -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,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhin.js/plugin-idiom-chain",
|
|
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": "tsc --build",
|
|
36
|
+
"clean": "rimraf lib"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/plugin.yml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Message, Plugin } from 'zhin.js';
|
|
2
|
+
import { channelKey } from '@zhin.js/game-shared';
|
|
3
|
+
import { continueGame, startGame } from './game-flow.js';
|
|
4
|
+
import { idiomCount, modeLabel, promptLine } from './engine.js';
|
|
5
|
+
import type { MatchMode } from './engine.js';
|
|
6
|
+
import type { SessionService } from './session-service.js';
|
|
7
|
+
|
|
8
|
+
export const CHAIN_HELP = [
|
|
9
|
+
'📜 成语接龙(文字接龙 · 四字成语)',
|
|
10
|
+
`开源词库 ${idiomCount()} 条 · 机器人先出第一句`,
|
|
11
|
+
'接龙 / chain — 帮助',
|
|
12
|
+
'接龙 开始 / 同音 — 同音接龙(默认,首字同音即可)',
|
|
13
|
+
'接龙 同字 — 严格同字接龙',
|
|
14
|
+
'接龙 继续 — 刷新界面',
|
|
15
|
+
'接龙 放弃 — 结束对局',
|
|
16
|
+
'',
|
|
17
|
+
'进行中:直接回复四字成语',
|
|
18
|
+
'按钮:提示 · 跳过 · 认输(失误 3 次判负)',
|
|
19
|
+
].join('\n');
|
|
20
|
+
|
|
21
|
+
export async function runChainCommand(
|
|
22
|
+
plugin: Plugin,
|
|
23
|
+
services: SessionService,
|
|
24
|
+
message: Message<any>,
|
|
25
|
+
action: string,
|
|
26
|
+
): Promise<string | undefined> {
|
|
27
|
+
const ch = channelKey(message);
|
|
28
|
+
const userId = message.$sender.id;
|
|
29
|
+
|
|
30
|
+
if (!action || action === 'help') {
|
|
31
|
+
const active = await services.getActiveByChannel(ch);
|
|
32
|
+
const lines = [CHAIN_HELP, ''];
|
|
33
|
+
if (active) {
|
|
34
|
+
const mode: MatchMode = active.match_mode === 'char' ? 'char' : 'pinyin';
|
|
35
|
+
lines.push(
|
|
36
|
+
`进行中:${active.player_name} · ${modeLabel(mode)} · 局分 ${active.player_score}:${active.bot_score} · 连击 ${active.streak}`,
|
|
37
|
+
);
|
|
38
|
+
if (active.last_idiom) lines.push(promptLine(active.last_idiom, mode));
|
|
39
|
+
} else {
|
|
40
|
+
lines.push('暂无对局,发送「接龙 开始」或「接龙 同字」。');
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (action === 'start_pinyin') return startGame(plugin, services, message, 'pinyin');
|
|
46
|
+
if (action === 'start_char') return startGame(plugin, services, message, 'char');
|
|
47
|
+
if (action === 'continue') return continueGame(plugin, services, message);
|
|
48
|
+
|
|
49
|
+
if (action === 'quit') {
|
|
50
|
+
const row = await services.getActiveForUser(ch, userId);
|
|
51
|
+
if (!row) return '你没有进行中的接龙。';
|
|
52
|
+
await services.updateSession(row.id, { status: 'aborted' });
|
|
53
|
+
return '已放弃成语接龙。';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `未知子命令:${action}\n\n${CHAIN_HELP}`;
|
|
57
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Message, MessageCommand, getActionFromMessage, type Plugin } from 'zhin.js';
|
|
2
|
+
import {
|
|
3
|
+
buildChoiceFallbackMap,
|
|
4
|
+
channelKey,
|
|
5
|
+
normalizeChainAction,
|
|
6
|
+
parseChoicePayload,
|
|
7
|
+
registerGameTextMiddleware,
|
|
8
|
+
} from '@zhin.js/game-shared';
|
|
9
|
+
import { CHAIN_PREFIX, handleChoice, processIdiomText } from './game-flow.js';
|
|
10
|
+
import { runChainCommand } from './chain-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 runChainCommand(plugin, services, message, normalizeChainAction(raw));
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerCommands(plugin: Plugin, getServices: () => SessionService | null): void {
|
|
32
|
+
registerPattern(plugin, 'chain [action:word]', '成语接龙(chain)', 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, CHAIN_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'];
|
|
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(`${CHAIN_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 (err) 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(`${CHAIN_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(CHAIN_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, CHAIN_PREFIX) : null;
|
|
98
|
+
if (parsed?.sessionId === session.id) {
|
|
99
|
+
const err = await handleChoice(plugin, services, message, session.id, parsed.choiceId);
|
|
100
|
+
if (err) await message.$reply?.(err);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (/^[\u4e00-\u9fff]{2,8}$/.test(raw)) {
|
|
106
|
+
await processIdiomText(plugin, services, message, raw);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return next();
|
|
111
|
+
}, 'idiom-chain:text');
|
|
112
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const CHAIN_PREFIX = 'chain';
|
|
2
|
+
|
|
3
|
+
export type { MatchMode, IdiomEntry, ValidateResult } from './idiom-provider.js';
|
|
4
|
+
export {
|
|
5
|
+
idiomCount,
|
|
6
|
+
normalizeInput,
|
|
7
|
+
isKnownIdiom,
|
|
8
|
+
getGloss,
|
|
9
|
+
lastChar,
|
|
10
|
+
lastSyllableNoTone,
|
|
11
|
+
modeLabel,
|
|
12
|
+
getValidNext,
|
|
13
|
+
pickBotIdiom,
|
|
14
|
+
pickStarterIdiom,
|
|
15
|
+
pickHintIdiom,
|
|
16
|
+
validatePlayerIdiom,
|
|
17
|
+
promptLine,
|
|
18
|
+
} from './idiom-provider.js';
|