@zhin.js/plugin-idiom-chain 1.1.17 → 1.1.19
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/commands/chain/[[action]]/index.js +5 -4
- package/commands/chain/[[action]]/index.ts +5 -4
- package/lib/chain-command.d.ts +3 -3
- package/lib/chain-command.js +33 -31
- package/lib/chain-command.js.map +1 -1
- package/lib/game-flow.d.ts +6 -6
- package/lib/game-flow.js +158 -147
- package/lib/game-flow.js.map +1 -1
- package/lib/session-service.d.ts +2 -2
- package/lib/session-service.js +4 -9
- package/lib/session-service.js.map +1 -1
- package/middlewares/chain-alias/index.js +12 -8
- package/middlewares/chain-alias/index.ts +11 -7
- package/middlewares/chain-text/index.js +13 -12
- package/middlewares/chain-text/index.ts +13 -11
- package/package.json +7 -7
- package/src/chain-command.ts +31 -29
- package/src/game-flow.ts +158 -148
- package/src/session-service.ts +6 -12
package/src/game-flow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createMessages, resolveLocale } from 'zhin.js/i18n';
|
|
2
|
-
import
|
|
2
|
+
import { channelKey, type GamePlayer, type GameReply } from '@zhin.js/game-kit';
|
|
3
3
|
import {
|
|
4
4
|
CHAIN_PREFIX,
|
|
5
5
|
getGloss,
|
|
@@ -23,12 +23,14 @@ function sessionMode(session: ChainSessionRow): MatchMode {
|
|
|
23
23
|
|
|
24
24
|
export async function sendOrEditView(
|
|
25
25
|
services: SessionService,
|
|
26
|
-
message:
|
|
26
|
+
message: GamePlayer,
|
|
27
27
|
session: ChainSessionRow,
|
|
28
28
|
eventLines: string[] = [],
|
|
29
29
|
): Promise<GameReply> {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
return services.runInScope({ scope: channelKey(message), run: async () => {
|
|
31
|
+
const content = buildChainView(session, eventLines, message.target.kind);
|
|
32
|
+
return content;
|
|
33
|
+
} });
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
async function botTurn(
|
|
@@ -67,192 +69,200 @@ async function botTurn(
|
|
|
67
69
|
|
|
68
70
|
export async function startGame(
|
|
69
71
|
services: SessionService,
|
|
70
|
-
message:
|
|
72
|
+
message: GamePlayer,
|
|
71
73
|
matchMode: MatchMode = 'pinyin',
|
|
72
74
|
): Promise<GameReply | undefined> {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (active
|
|
78
|
-
|
|
75
|
+
return services.runInScope({ scope: channelKey(message), run: async () => {
|
|
76
|
+
const ch = channelKey(message);
|
|
77
|
+
const active = await services.getActiveByChannel(ch);
|
|
78
|
+
const locale = resolveLocale(active ? active.locale : message.locale);
|
|
79
|
+
if (active) {
|
|
80
|
+
if (active.player_id === message.sender.id) {
|
|
81
|
+
return messages(locale, 'text3');
|
|
82
|
+
}
|
|
83
|
+
return messages(locale, 'text4', { p0: active.player_name });
|
|
79
84
|
}
|
|
80
|
-
return messages(locale, 'text4', { p0: active.player_name });
|
|
81
|
-
}
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
const used = new Set<string>();
|
|
87
|
+
const starter = pickStarterIdiom(used);
|
|
88
|
+
used.add(starter.text);
|
|
89
|
+
const session = await services.createSession(message, {
|
|
90
|
+
text: starter.text,
|
|
91
|
+
nextChar: lastChar(starter.text),
|
|
92
|
+
used: [...used],
|
|
93
|
+
matchMode,
|
|
94
|
+
});
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const gloss = starter.gloss ?? getGloss(starter.text);
|
|
97
|
+
return sendOrEditView(services, message, session, [
|
|
98
|
+
messages(locale, 'text5', { p0: modeLabel(matchMode, locale), p1: starter.text }) + (gloss ? `(${gloss})` : ''),
|
|
99
|
+
promptLine(starter.text, matchMode, locale),
|
|
100
|
+
]);
|
|
101
|
+
} });
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
export async function continueGame(
|
|
101
105
|
services: SessionService,
|
|
102
|
-
message:
|
|
106
|
+
message: GamePlayer,
|
|
103
107
|
): Promise<GameReply> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
return services.runInScope({ scope: channelKey(message), run: async () => {
|
|
109
|
+
const session = await services.getActiveForUser(
|
|
110
|
+
channelKey(message),
|
|
111
|
+
message.sender.id,
|
|
112
|
+
);
|
|
113
|
+
const locale = resolveLocale(session ? session.locale : message.locale);
|
|
114
|
+
if (!session) return messages(locale, 'text6');
|
|
115
|
+
return sendOrEditView(services, message, session);
|
|
116
|
+
} });
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
export async function processIdiomText(
|
|
114
120
|
services: SessionService,
|
|
115
|
-
message:
|
|
121
|
+
message: GamePlayer,
|
|
116
122
|
raw: string,
|
|
117
123
|
): Promise<GameReply | null> {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
return services.runInScope({ scope: channelKey(message), run: async () => {
|
|
125
|
+
const ch = channelKey(message);
|
|
126
|
+
const session = await services.getActiveForUser(ch, message.sender.id);
|
|
127
|
+
const locale = resolveLocale(session ? session.locale : message.locale);
|
|
128
|
+
if (!session || session.status !== 'active') return null;
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
const mode = sessionMode(session);
|
|
131
|
+
const used = parseUsed(session.used_idioms);
|
|
132
|
+
const idiom = normalizeInput(raw);
|
|
133
|
+
// 格式不合规(非四字)只提示、不扣失误:避免闲聊(「好的」「谢谢」)被累计判负
|
|
134
|
+
if (!/^[\u4e00-\u9fff]{4}$/.test(idiom)) {
|
|
135
|
+
return messages(locale, 'text7');
|
|
136
|
+
}
|
|
137
|
+
const check = validatePlayerIdiom(idiom, session.last_idiom, mode, used, locale);
|
|
138
|
+
if (!check.ok) {
|
|
139
|
+
const wrong = session.wrong_count + 1;
|
|
140
|
+
if (wrong >= MAX_WRONG) {
|
|
141
|
+
await services.updateSession(session.id, {
|
|
142
|
+
wrong_count: wrong,
|
|
143
|
+
status: 'lost',
|
|
144
|
+
bot_score: session.bot_score + 1,
|
|
145
|
+
streak: 0,
|
|
146
|
+
});
|
|
147
|
+
const updated = (await services.getById(session.id))!;
|
|
148
|
+
return (await sendOrEditView(services, message, updated, [
|
|
149
|
+
check.reason!,
|
|
150
|
+
messages(locale, 'text8'),
|
|
151
|
+
])) ?? null;
|
|
152
|
+
}
|
|
153
|
+
await services.updateSession(session.id, { wrong_count: wrong, streak: 0 });
|
|
140
154
|
const updated = (await services.getById(session.id))!;
|
|
141
|
-
return (await sendOrEditView(services, message, updated, [
|
|
142
|
-
check.reason!,
|
|
143
|
-
messages(locale, 'text8'),
|
|
144
|
-
])) ?? null;
|
|
155
|
+
return (await sendOrEditView(services, message, updated, [check.reason!])) ?? null;
|
|
145
156
|
}
|
|
146
|
-
await services.updateSession(session.id, { wrong_count: wrong, streak: 0 });
|
|
147
|
-
const updated = (await services.getById(session.id))!;
|
|
148
|
-
return (await sendOrEditView(services, message, updated, [check.reason!])) ?? null;
|
|
149
|
-
}
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
used.add(idiom);
|
|
159
|
+
const streak = session.streak + 1;
|
|
160
|
+
const best = Math.max(session.best_streak, streak);
|
|
161
|
+
const nextChar = lastChar(idiom);
|
|
162
|
+
const gloss = getGloss(idiom);
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
await services.updateSession(session.id, {
|
|
165
|
+
last_idiom: idiom,
|
|
166
|
+
next_char: nextChar,
|
|
167
|
+
used_idioms: JSON.stringify([...used]),
|
|
168
|
+
streak,
|
|
169
|
+
best_streak: best,
|
|
170
|
+
wrong_count: 0,
|
|
171
|
+
});
|
|
165
172
|
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
const current = (await services.getById(session.id))!;
|
|
174
|
+
const userLine = messages(locale, 'text9', { p0: idiom }) + (gloss ? `(${gloss})` : '');
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
const botResult = await botTurn(services, current, used);
|
|
177
|
+
return (await sendOrEditView(services, message, botResult.session, [
|
|
178
|
+
userLine,
|
|
179
|
+
...botResult.lines,
|
|
180
|
+
])) ?? null;
|
|
181
|
+
} });
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
export async function handleChoice(
|
|
177
185
|
services: SessionService,
|
|
178
|
-
message:
|
|
186
|
+
message: GamePlayer,
|
|
179
187
|
sessionId: string,
|
|
180
188
|
choiceId: string,
|
|
181
189
|
): Promise<GameReply | null> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
return services.runInScope({ scope: channelKey(message), run: async () => {
|
|
191
|
+
const session = await services.getById(sessionId);
|
|
192
|
+
const locale = resolveLocale(session ? session.locale : message.locale);
|
|
193
|
+
if (!session) return messages(locale, 'text10');
|
|
194
|
+
if (session.player_id !== message.sender.id) return messages(locale, 'text11');
|
|
186
195
|
|
|
187
|
-
|
|
196
|
+
const mode = sessionMode(session);
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
if (choiceId === 'restart') {
|
|
199
|
+
const locale = resolveLocale(message.locale);
|
|
200
|
+
const used = new Set<string>();
|
|
201
|
+
const starter = pickStarterIdiom(used);
|
|
202
|
+
used.add(starter.text);
|
|
203
|
+
await services.updateSession(session.id, {
|
|
204
|
+
locale,
|
|
205
|
+
last_idiom: starter.text,
|
|
206
|
+
next_char: lastChar(starter.text),
|
|
207
|
+
used_idioms: JSON.stringify([...used]),
|
|
208
|
+
streak: 0,
|
|
209
|
+
wrong_count: 0,
|
|
210
|
+
turn: 'player',
|
|
211
|
+
status: 'active',
|
|
212
|
+
});
|
|
213
|
+
const updated = (await services.getById(session.id))!;
|
|
214
|
+
const gloss = starter.gloss ?? getGloss(starter.text);
|
|
215
|
+
return (await sendOrEditView(services, message, updated, [
|
|
216
|
+
messages(locale, 'text12', { p0: starter.text }) + (gloss ? `(${gloss})` : ''),
|
|
217
|
+
promptLine(starter.text, mode, locale),
|
|
218
|
+
])) ?? null;
|
|
219
|
+
}
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
if (session.status !== 'active') {
|
|
222
|
+
return messages(locale, 'text13');
|
|
223
|
+
}
|
|
215
224
|
|
|
216
|
-
|
|
225
|
+
const used = parseUsed(session.used_idioms);
|
|
217
226
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
if (choiceId === 'hint') {
|
|
228
|
+
const hint = pickHintIdiom(session.last_idiom, mode, used);
|
|
229
|
+
if (!hint) {
|
|
230
|
+
// 先更新状态再单次发送终局视图,避免旧 session「你赢了」与终局视图双发
|
|
231
|
+
await services.updateSession(session.id, { status: 'won', player_score: session.player_score + 1 });
|
|
232
|
+
const updated = (await services.getById(session.id))!;
|
|
233
|
+
return (await sendOrEditView(services, message, updated, [
|
|
234
|
+
messages(locale, 'text14'),
|
|
235
|
+
])) ?? null;
|
|
236
|
+
}
|
|
237
|
+
const gloss = getGloss(hint);
|
|
238
|
+
await services.updateSession(session.id, { hints_used: session.hints_used + 1, streak: 0 });
|
|
223
239
|
const updated = (await services.getById(session.id))!;
|
|
224
240
|
return (await sendOrEditView(services, message, updated, [
|
|
225
|
-
messages(locale, '
|
|
241
|
+
messages(locale, 'text15', { p0: hint }) + (gloss ? `(${gloss})` : ''),
|
|
242
|
+
messages(locale, 'text16'),
|
|
226
243
|
])) ?? null;
|
|
227
244
|
}
|
|
228
|
-
const gloss = getGloss(hint);
|
|
229
|
-
await services.updateSession(session.id, { hints_used: session.hints_used + 1, streak: 0 });
|
|
230
|
-
const updated = (await services.getById(session.id))!;
|
|
231
|
-
return (await sendOrEditView(services, message, updated, [
|
|
232
|
-
messages(locale, 'text15', { p0: hint }) + (gloss ? `(${gloss})` : ''),
|
|
233
|
-
messages(locale, 'text16'),
|
|
234
|
-
])) ?? null;
|
|
235
|
-
}
|
|
236
245
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
if (choiceId === 'skip') {
|
|
247
|
+
await services.updateSession(session.id, {
|
|
248
|
+
status: 'lost',
|
|
249
|
+
bot_score: session.bot_score + 1,
|
|
250
|
+
streak: 0,
|
|
251
|
+
});
|
|
252
|
+
const updated = (await services.getById(session.id))!;
|
|
253
|
+
return (await sendOrEditView(services, message, updated, [
|
|
254
|
+
messages(locale, 'text17'),
|
|
255
|
+
])) ?? null;
|
|
256
|
+
}
|
|
248
257
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
if (choiceId === 'quit') {
|
|
259
|
+
await services.updateSession(session.id, { status: 'lost', bot_score: session.bot_score + 1 });
|
|
260
|
+
const updated = (await services.getById(session.id))!;
|
|
261
|
+
return (await sendOrEditView(services, message, updated, [messages(locale, 'text18')])) ?? null;
|
|
262
|
+
}
|
|
254
263
|
|
|
255
|
-
|
|
264
|
+
return messages(locale, 'text19');
|
|
265
|
+
} });
|
|
256
266
|
}
|
|
257
267
|
|
|
258
268
|
export { CHAIN_PREFIX };
|
package/src/session-service.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Database, Models } from '@zhin.js/core';
|
|
2
2
|
import {
|
|
3
3
|
BaseSessionService,
|
|
4
|
-
channelKey,
|
|
5
4
|
generateSessionId,
|
|
6
|
-
type
|
|
5
|
+
type GamePlayer,
|
|
7
6
|
type GameSessionDatabase,
|
|
8
7
|
} from '@zhin.js/game-kit';
|
|
9
8
|
import type { MatchMode } from './engine.js';
|
|
@@ -36,20 +35,15 @@ export class SessionService extends BaseSessionService<ChainSessionRow> {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
async createSession(
|
|
39
|
-
message:
|
|
38
|
+
message: GamePlayer,
|
|
40
39
|
starter: { text: string; nextChar: string; used: string[]; matchMode: MatchMode },
|
|
41
40
|
): Promise<ChainSessionRow> {
|
|
42
41
|
const now = Date.now();
|
|
43
|
-
const row: ChainSessionRow = {
|
|
42
|
+
const row: Omit<ChainSessionRow, 'channel_key' | 'adapter' | 'endpoint' | 'channel_type' | 'channel_id'> = {
|
|
44
43
|
id: generateSessionId(),
|
|
45
44
|
locale: message.locale ?? 'zh-CN',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
channel_type: message.$channel.type,
|
|
49
|
-
channel_id: message.$channel.id,
|
|
50
|
-
channel_key: channelKey(message),
|
|
51
|
-
player_id: message.$sender.id,
|
|
52
|
-
player_name: message.$sender.name?.trim() || message.$sender.id,
|
|
45
|
+
player_id: message.sender.id,
|
|
46
|
+
player_name: message.sender.name?.trim() || message.sender.id,
|
|
53
47
|
last_idiom: starter.text,
|
|
54
48
|
next_char: starter.nextChar,
|
|
55
49
|
match_mode: starter.matchMode,
|
|
@@ -65,7 +59,7 @@ export class SessionService extends BaseSessionService<ChainSessionRow> {
|
|
|
65
59
|
updated_at: now,
|
|
66
60
|
created_at: now,
|
|
67
61
|
};
|
|
68
|
-
return this.createRow(row);
|
|
62
|
+
return this.createRow({ target: message.target, fields: row });
|
|
69
63
|
}
|
|
70
64
|
}
|
|
71
65
|
|