@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/src/game-flow.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createMessages, resolveLocale } from 'zhin.js/i18n';
2
- import type { GameMessageLike, GameReply } from '@zhin.js/game-kit';
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: GameMessageLike,
26
+ message: GamePlayer,
27
27
  session: ChainSessionRow,
28
28
  eventLines: string[] = [],
29
29
  ): Promise<GameReply> {
30
- const content = buildChainView(session, eventLines, message.$channel.type);
31
- return content;
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: GameMessageLike,
72
+ message: GamePlayer,
71
73
  matchMode: MatchMode = 'pinyin',
72
74
  ): Promise<GameReply | undefined> {
73
- const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
74
- const active = await services.getActiveByChannel(ch);
75
- const locale = resolveLocale(active ? active.locale : message.locale);
76
- if (active) {
77
- if (active.player_id === message.$sender.id) {
78
- return messages(locale, 'text3');
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
- const used = new Set<string>();
84
- const starter = pickStarterIdiom(used);
85
- used.add(starter.text);
86
- const session = await services.createSession(message, {
87
- text: starter.text,
88
- nextChar: lastChar(starter.text),
89
- used: [...used],
90
- matchMode,
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
- const gloss = starter.gloss ?? getGloss(starter.text);
94
- return sendOrEditView(services, message, session, [
95
- messages(locale, 'text5', { p0: modeLabel(matchMode, locale), p1: starter.text }) + (gloss ? `(${gloss})` : ''),
96
- promptLine(starter.text, matchMode, locale),
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: GameMessageLike,
106
+ message: GamePlayer,
103
107
  ): Promise<GameReply> {
104
- const session = await services.getActiveForUser(
105
- `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`,
106
- message.$sender.id,
107
- );
108
- const locale = resolveLocale(session ? session.locale : message.locale);
109
- if (!session) return messages(locale, 'text6');
110
- return sendOrEditView(services, message, session);
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: GameMessageLike,
121
+ message: GamePlayer,
116
122
  raw: string,
117
123
  ): Promise<GameReply | null> {
118
- const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
119
- const session = await services.getActiveForUser(ch, message.$sender.id);
120
- const locale = resolveLocale(session ? session.locale : message.locale);
121
- if (!session || session.status !== 'active') return null;
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
- const mode = sessionMode(session);
124
- const used = parseUsed(session.used_idioms);
125
- const idiom = normalizeInput(raw);
126
- // 格式不合规(非四字)只提示、不扣失误:避免闲聊(「好的」「谢谢」)被累计判负
127
- if (!/^[\u4e00-\u9fff]{4}$/.test(idiom)) {
128
- return messages(locale, 'text7');
129
- }
130
- const check = validatePlayerIdiom(idiom, session.last_idiom, mode, used, locale);
131
- if (!check.ok) {
132
- const wrong = session.wrong_count + 1;
133
- if (wrong >= MAX_WRONG) {
134
- await services.updateSession(session.id, {
135
- wrong_count: wrong,
136
- status: 'lost',
137
- bot_score: session.bot_score + 1,
138
- streak: 0,
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
- used.add(idiom);
152
- const streak = session.streak + 1;
153
- const best = Math.max(session.best_streak, streak);
154
- const nextChar = lastChar(idiom);
155
- const gloss = getGloss(idiom);
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
- await services.updateSession(session.id, {
158
- last_idiom: idiom,
159
- next_char: nextChar,
160
- used_idioms: JSON.stringify([...used]),
161
- streak,
162
- best_streak: best,
163
- wrong_count: 0,
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
- const current = (await services.getById(session.id))!;
167
- const userLine = messages(locale, 'text9', { p0: idiom }) + (gloss ? `(${gloss})` : '');
173
+ const current = (await services.getById(session.id))!;
174
+ const userLine = messages(locale, 'text9', { p0: idiom }) + (gloss ? `(${gloss})` : '');
168
175
 
169
- const botResult = await botTurn(services, current, used);
170
- return (await sendOrEditView(services, message, botResult.session, [
171
- userLine,
172
- ...botResult.lines,
173
- ])) ?? null;
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: GameMessageLike,
186
+ message: GamePlayer,
179
187
  sessionId: string,
180
188
  choiceId: string,
181
189
  ): Promise<GameReply | null> {
182
- const session = await services.getById(sessionId);
183
- const locale = resolveLocale(session ? session.locale : message.locale);
184
- if (!session) return messages(locale, 'text10');
185
- if (session.player_id !== message.$sender.id) return messages(locale, 'text11');
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
- const mode = sessionMode(session);
196
+ const mode = sessionMode(session);
188
197
 
189
- if (choiceId === 'restart') {
190
- const locale = resolveLocale(message.locale);
191
- const used = new Set<string>();
192
- const starter = pickStarterIdiom(used);
193
- used.add(starter.text);
194
- await services.updateSession(session.id, {
195
- locale,
196
- last_idiom: starter.text,
197
- next_char: lastChar(starter.text),
198
- used_idioms: JSON.stringify([...used]),
199
- streak: 0,
200
- wrong_count: 0,
201
- turn: 'player',
202
- status: 'active',
203
- });
204
- const updated = (await services.getById(session.id))!;
205
- const gloss = starter.gloss ?? getGloss(starter.text);
206
- return (await sendOrEditView(services, message, updated, [
207
- messages(locale, 'text12', { p0: starter.text }) + (gloss ? `(${gloss})` : ''),
208
- promptLine(starter.text, mode, locale),
209
- ])) ?? null;
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
- if (session.status !== 'active') {
213
- return messages(locale, 'text13');
214
- }
221
+ if (session.status !== 'active') {
222
+ return messages(locale, 'text13');
223
+ }
215
224
 
216
- const used = parseUsed(session.used_idioms);
225
+ const used = parseUsed(session.used_idioms);
217
226
 
218
- if (choiceId === 'hint') {
219
- const hint = pickHintIdiom(session.last_idiom, mode, used);
220
- if (!hint) {
221
- // 先更新状态再单次发送终局视图,避免旧 session「你赢了」与终局视图双发
222
- await services.updateSession(session.id, { status: 'won', player_score: session.player_score + 1 });
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, 'text14'),
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
- if (choiceId === 'skip') {
238
- await services.updateSession(session.id, {
239
- status: 'lost',
240
- bot_score: session.bot_score + 1,
241
- streak: 0,
242
- });
243
- const updated = (await services.getById(session.id))!;
244
- return (await sendOrEditView(services, message, updated, [
245
- messages(locale, 'text17'),
246
- ])) ?? null;
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
- if (choiceId === 'quit') {
250
- await services.updateSession(session.id, { status: 'lost', bot_score: session.bot_score + 1 });
251
- const updated = (await services.getById(session.id))!;
252
- return (await sendOrEditView(services, message, updated, [messages(locale, 'text18')])) ?? null;
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
- return messages(locale, 'text19');
264
+ return messages(locale, 'text19');
265
+ } });
256
266
  }
257
267
 
258
268
  export { CHAIN_PREFIX };
@@ -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 GameMessageLike,
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: GameMessageLike,
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
- adapter: String(message.$adapter),
47
- endpoint: message.$endpoint,
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