@zhin.js/plugin-guess-number 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/commands.d.ts +5 -0
- package/lib/commands.d.ts.map +1 -0
- package/lib/commands.js +38 -0
- package/lib/commands.js.map +1 -0
- package/lib/engine.d.ts +9 -0
- package/lib/engine.d.ts.map +1 -0
- package/lib/engine.js +20 -0
- package/lib/engine.js.map +1 -0
- package/lib/game-flow.d.ts +5 -0
- package/lib/game-flow.d.ts.map +1 -0
- package/lib/game-flow.js +54 -0
- package/lib/game-flow.js.map +1 -0
- package/lib/guess-command.d.ts +5 -0
- package/lib/guess-command.d.ts.map +1 -0
- package/lib/guess-command.js +37 -0
- package/lib/guess-command.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 +28 -0
- package/lib/hub-register.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +25 -0
- package/lib/index.js.map +1 -0
- package/lib/models.d.ts +27 -0
- package/lib/models.d.ts.map +1 -0
- package/lib/models.js +21 -0
- package/lib/models.js.map +1 -0
- package/lib/session-service.d.ts +17 -0
- package/lib/session-service.d.ts.map +1 -0
- package/lib/session-service.js +86 -0
- package/lib/session-service.js.map +1 -0
- package/package.json +37 -0
- package/plugin.yml +2 -0
- package/src/commands.ts +46 -0
- package/src/engine.ts +21 -0
- package/src/game-flow.ts +68 -0
- package/src/guess-command.ts +44 -0
- package/src/hub-register.ts +29 -0
- package/src/index.ts +35 -0
- package/src/models.ts +49 -0
- package/src/session-service.ts +96 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Plugin } from 'zhin.js';
|
|
2
|
+
import type { SessionService } from './session-service.js';
|
|
3
|
+
export declare function registerCommands(plugin: Plugin, getServices: () => SessionService | null): void;
|
|
4
|
+
export declare function registerGuessMiddleware(plugin: Plugin, getServices: () => SessionService | null): void;
|
|
5
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoB3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAG/F;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAgBtG"}
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MessageCommand } from 'zhin.js';
|
|
2
|
+
import { channelKey, normalizeGuessAction, registerGameTextMiddleware } from '@zhin.js/game-shared';
|
|
3
|
+
import { processGuess } from './game-flow.js';
|
|
4
|
+
import { runGuessCommand } from './guess-command.js';
|
|
5
|
+
function registerPattern(plugin, pattern, desc, getServices) {
|
|
6
|
+
plugin.addCommand(new MessageCommand(pattern)
|
|
7
|
+
.desc(desc)
|
|
8
|
+
.action(async (message, result) => {
|
|
9
|
+
const services = getServices();
|
|
10
|
+
if (!services)
|
|
11
|
+
return '猜数字需要启用 database 配置。';
|
|
12
|
+
const raw = result.params.action ?? '';
|
|
13
|
+
return runGuessCommand(services, message, normalizeGuessAction(raw));
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export function registerCommands(plugin, getServices) {
|
|
17
|
+
registerPattern(plugin, 'guess [action:word]', '猜数字(guess)', getServices);
|
|
18
|
+
registerPattern(plugin, '猜数 [action:word]', '猜数字(中文)', getServices);
|
|
19
|
+
}
|
|
20
|
+
export function registerGuessMiddleware(plugin, getServices) {
|
|
21
|
+
registerGameTextMiddleware(plugin, async (message, next) => {
|
|
22
|
+
const services = getServices();
|
|
23
|
+
if (!services)
|
|
24
|
+
return next();
|
|
25
|
+
const raw = message.$raw?.trim() ?? '';
|
|
26
|
+
const m = /^(\d+)$/.exec(raw);
|
|
27
|
+
if (!m)
|
|
28
|
+
return next();
|
|
29
|
+
const ch = channelKey(message);
|
|
30
|
+
const session = await services.getActiveForUser(ch, message.$sender.id);
|
|
31
|
+
if (!session)
|
|
32
|
+
return next();
|
|
33
|
+
const reply = await processGuess(services, message, Number(m[1]));
|
|
34
|
+
if (reply)
|
|
35
|
+
await message.$reply?.(reply);
|
|
36
|
+
}, 'guess-number:text');
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,cAAc,EAAe,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,SAAS,eAAe,CACtB,MAAc,EACd,OAAe,EACf,IAAY,EACZ,WAAwC;IAExC,MAAM,CAAC,UAAU,CACf,IAAI,cAAc,CAAC,OAAO,CAAC;SACxB,IAAI,CAAC,IAAI,CAAC;SACV,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC,QAAQ;YAAE,OAAO,sBAAsB,CAAC;QAC7C,MAAM,GAAG,GAAI,MAAM,CAAC,MAAM,CAAC,MAA6B,IAAI,EAAE,CAAC;QAC/D,OAAO,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC;IACvE,CAAC,CAAC,CACL,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,WAAwC;IACvF,eAAe,CAAC,MAAM,EAAE,qBAAqB,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;IAC1E,eAAe,CAAC,MAAM,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAc,EAAE,WAAwC;IAC9F,0BAA0B,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QACzD,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,EAAE,CAAC;QAE7B,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACvC,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QAEtB,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,gBAAgB,CAAC,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,EAAE,CAAC;QAE5B,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClE,IAAI,KAAK;YAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC,EAAE,mBAAmB,CAAC,CAAC;AAC1B,CAAC"}
|
package/lib/engine.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const GUESS_PREFIX = "guess";
|
|
2
|
+
export declare const MIN = 1;
|
|
3
|
+
export declare const MAX = 100;
|
|
4
|
+
export declare const MAX_ATTEMPTS = 7;
|
|
5
|
+
export type GuessResult = 'win' | 'low' | 'high' | 'invalid';
|
|
6
|
+
export declare function newSecret(): number;
|
|
7
|
+
export declare function evaluateGuess(secret: number, value: number): GuessResult;
|
|
8
|
+
export declare function hintText(result: Exclude<GuessResult, 'win' | 'invalid'>, rangeMin: number, rangeMax: number): string;
|
|
9
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,UAAU,CAAC;AACpC,eAAO,MAAM,GAAG,IAAI,CAAC;AACrB,eAAO,MAAM,GAAG,MAAM,CAAC;AACvB,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7D,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAIxE;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGpH"}
|
package/lib/engine.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const GUESS_PREFIX = 'guess';
|
|
2
|
+
export const MIN = 1;
|
|
3
|
+
export const MAX = 100;
|
|
4
|
+
export const MAX_ATTEMPTS = 7;
|
|
5
|
+
export function newSecret() {
|
|
6
|
+
return MIN + Math.floor(Math.random() * (MAX - MIN + 1));
|
|
7
|
+
}
|
|
8
|
+
export function evaluateGuess(secret, value) {
|
|
9
|
+
if (!Number.isInteger(value) || value < MIN || value > MAX)
|
|
10
|
+
return 'invalid';
|
|
11
|
+
if (value === secret)
|
|
12
|
+
return 'win';
|
|
13
|
+
return value < secret ? 'low' : 'high';
|
|
14
|
+
}
|
|
15
|
+
export function hintText(result, rangeMin, rangeMax) {
|
|
16
|
+
if (result === 'low')
|
|
17
|
+
return `📈 **太小了!** 范围 ${rangeMin} ~ ${rangeMax}`;
|
|
18
|
+
return `📉 **太大了!** 范围 ${rangeMin} ~ ${rangeMax}`;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC;AACpC,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;AACrB,MAAM,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC;AACvB,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAI9B,MAAM,UAAU,SAAS;IACvB,OAAO,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAa;IACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,GAAG,IAAI,KAAK,GAAG,GAAG;QAAE,OAAO,SAAS,CAAC;IAC7E,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAA+C,EAAE,QAAgB,EAAE,QAAgB;IAC1G,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,kBAAkB,QAAQ,MAAM,QAAQ,EAAE,CAAC;IACxE,OAAO,kBAAkB,QAAQ,MAAM,QAAQ,EAAE,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Message } from 'zhin.js';
|
|
2
|
+
import type { SessionService } from './session-service.js';
|
|
3
|
+
export declare function startGame(services: SessionService, message: Message<any>): Promise<string>;
|
|
4
|
+
export declare function processGuess(services: SessionService, message: Message<any>, value: number): Promise<string | null>;
|
|
5
|
+
//# sourceMappingURL=game-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-flow.d.ts","sourceRoot":"","sources":["../src/game-flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GACpB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB"}
|
package/lib/game-flow.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { evaluateGuess, hintText, MAX, MIN } from './engine.js';
|
|
2
|
+
import { formatStatus } from './session-service.js';
|
|
3
|
+
export async function startGame(services, message) {
|
|
4
|
+
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
5
|
+
const mine = await services.getActiveForUser(ch, message.$sender.id);
|
|
6
|
+
if (mine) {
|
|
7
|
+
return `${formatStatus(mine)}\n\n(你已有进行中的局,继续猜数字,或发送「猜数 放弃」)`;
|
|
8
|
+
}
|
|
9
|
+
const session = await services.createSession(message);
|
|
10
|
+
return formatStatus(session);
|
|
11
|
+
}
|
|
12
|
+
export async function processGuess(services, message, value) {
|
|
13
|
+
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
14
|
+
const session = await services.getActiveForUser(ch, message.$sender.id);
|
|
15
|
+
if (!session)
|
|
16
|
+
return null;
|
|
17
|
+
const result = evaluateGuess(session.secret, value);
|
|
18
|
+
if (result === 'invalid') {
|
|
19
|
+
return `请输入 ${MIN} ~ ${MAX} 之间的整数。`;
|
|
20
|
+
}
|
|
21
|
+
const attempts = session.attempts + 1;
|
|
22
|
+
if (result === 'win') {
|
|
23
|
+
await services.updateSession(session.id, { attempts, status: 'won' });
|
|
24
|
+
return [
|
|
25
|
+
'🎉 **猜对了!**',
|
|
26
|
+
'',
|
|
27
|
+
`答案就是 **${session.secret}**,你用了 ${attempts} 次。`,
|
|
28
|
+
'发送「猜数 开始」再来一局。',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
if (attempts >= session.max_attempts) {
|
|
32
|
+
await services.updateSession(session.id, { attempts, status: 'lost' });
|
|
33
|
+
return [
|
|
34
|
+
'💀 **机会用完了!**',
|
|
35
|
+
'',
|
|
36
|
+
`正确答案是 **${session.secret}**。`,
|
|
37
|
+
'发送「猜数 开始」再来一局。',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
let rangeMin = session.range_min;
|
|
41
|
+
let rangeMax = session.range_max;
|
|
42
|
+
if (result === 'low')
|
|
43
|
+
rangeMin = Math.max(rangeMin, value + 1);
|
|
44
|
+
else
|
|
45
|
+
rangeMax = Math.min(rangeMax, value - 1);
|
|
46
|
+
await services.updateSession(session.id, { attempts, range_min: rangeMin, range_max: rangeMax });
|
|
47
|
+
const left = session.max_attempts - attempts;
|
|
48
|
+
return [
|
|
49
|
+
hintText(result, rangeMin, rangeMax),
|
|
50
|
+
'',
|
|
51
|
+
`剩余 **${left}** 次机会,继续猜吧!`,
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=game-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-flow.js","sourceRoot":"","sources":["../src/game-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Message } from 'zhin.js';
|
|
2
|
+
import { type SessionService } from './session-service.js';
|
|
3
|
+
export declare const GUESS_HELP: string;
|
|
4
|
+
export declare function runGuessCommand(services: SessionService, message: Message<any>, action: string): Promise<string>;
|
|
5
|
+
//# sourceMappingURL=guess-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guess-command.d.ts","sourceRoot":"","sources":["../src/guess-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,MAAM,SAAS,CAAC;AAG/C,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEzE,eAAO,MAAM,UAAU,QAOX,CAAC;AAEb,wBAAsB,eAAe,CACnC,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAyBjB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { channelKey } from '@zhin.js/game-shared';
|
|
2
|
+
import { startGame } from './game-flow.js';
|
|
3
|
+
import { formatStatus } from './session-service.js';
|
|
4
|
+
export const GUESS_HELP = [
|
|
5
|
+
'🔢 猜数字(1~100,7 次机会)',
|
|
6
|
+
'猜数 / guess — 帮助',
|
|
7
|
+
'猜数 开始 — 新一局',
|
|
8
|
+
'猜数 放弃 — 结束当前局',
|
|
9
|
+
'',
|
|
10
|
+
'进行中直接回复数字即可。',
|
|
11
|
+
].join('\n');
|
|
12
|
+
export async function runGuessCommand(services, message, action) {
|
|
13
|
+
const ch = channelKey(message);
|
|
14
|
+
const userId = message.$sender.id;
|
|
15
|
+
if (!action || action === 'help') {
|
|
16
|
+
const active = await services.getActiveForUser(ch, userId);
|
|
17
|
+
const lines = [GUESS_HELP, ''];
|
|
18
|
+
if (active) {
|
|
19
|
+
lines.push(formatStatus(active));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
lines.push('暂无对局,发送「猜数 开始」。');
|
|
23
|
+
}
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
if (action === 'start')
|
|
27
|
+
return startGame(services, message);
|
|
28
|
+
if (action === 'quit') {
|
|
29
|
+
const row = await services.getActiveForUser(ch, userId);
|
|
30
|
+
if (!row)
|
|
31
|
+
return '你没有进行中的猜数字。';
|
|
32
|
+
await services.updateSession(row.id, { status: 'aborted' });
|
|
33
|
+
return '已放弃本局猜数字。';
|
|
34
|
+
}
|
|
35
|
+
return `未知子命令:${action}\n\n${GUESS_HELP}`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=guess-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guess-command.js","sourceRoot":"","sources":["../src/guess-command.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAuB,MAAM,sBAAsB,CAAC;AAEzE,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,qBAAqB;IACrB,iBAAiB;IACjB,aAAa;IACb,eAAe;IACf,EAAE;IACF,cAAc;CACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAwB,EACxB,OAAqB,EACrB,MAAc;IAEd,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;IAElC,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,gBAAgB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC/B,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAE5D,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,gBAAgB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACxD,IAAI,CAAC,GAAG;YAAE,OAAO,aAAa,CAAC;QAC/B,MAAM,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAC5D,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,OAAO,SAAS,MAAM,OAAO,UAAU,EAAE,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { GUESS_HELP } from './guess-command.js';
|
|
2
|
+
import type { SessionService } from './session-service.js';
|
|
3
|
+
export declare function registerGuessHub(getServices: () => SessionService | null): () => void;
|
|
4
|
+
export { GUESS_HELP };
|
|
5
|
+
//# sourceMappingURL=hub-register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-register.d.ts","sourceRoot":"","sources":["../src/hub-register.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,MAAM,IAAI,CAqBrF;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getPlugin } from 'zhin.js';
|
|
2
|
+
import { ensureGameHubService } from '@zhin.js/game-shared';
|
|
3
|
+
import { runGuessCommand, GUESS_HELP } from './guess-command.js';
|
|
4
|
+
export function registerGuessHub(getServices) {
|
|
5
|
+
const plugin = getPlugin();
|
|
6
|
+
ensureGameHubService(plugin);
|
|
7
|
+
return plugin.registerGame({
|
|
8
|
+
id: 'guess',
|
|
9
|
+
title: '猜数字',
|
|
10
|
+
icon: '🔢',
|
|
11
|
+
description: '1~100 七步猜中神秘数',
|
|
12
|
+
commandPrefix: '猜数',
|
|
13
|
+
quickStart: '开始',
|
|
14
|
+
aliases: ['guess'],
|
|
15
|
+
menus: [
|
|
16
|
+
{ id: 'start', label: '🎮 开始新局', style: 'primary' },
|
|
17
|
+
{ id: 'help', label: '📖 玩法说明' },
|
|
18
|
+
],
|
|
19
|
+
runAction: async (actionId, ctx) => {
|
|
20
|
+
const services = getServices();
|
|
21
|
+
if (!services)
|
|
22
|
+
return '猜数字需要启用 database 配置。';
|
|
23
|
+
return runGuessCommand(services, ctx.message, actionId);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export { GUESS_HELP };
|
|
28
|
+
//# sourceMappingURL=hub-register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-register.js","sourceRoot":"","sources":["../src/hub-register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGjE,MAAM,UAAU,gBAAgB,CAAC,WAAwC;IACvE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,YAAY,CAAC;QACzB,EAAE,EAAE,OAAO;QACX,KAAK,EAAE,KAAK;QACZ,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,eAAe;QAC5B,aAAa,EAAE,IAAI;QACnB,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,CAAC,OAAO,CAAC;QAClB,KAAK,EAAE;YACL,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;YACnD,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE;SACjC;QACD,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE;YACjC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;YAC/B,IAAI,CAAC,QAAQ;gBAAE,OAAO,sBAAsB,CAAC;YAC7C,OAAO,eAAe,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC1D,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Cron, formatCompact, usePlugin } from 'zhin.js';
|
|
2
|
+
import { registerModels } from './models.js';
|
|
3
|
+
import { createServices, resolveGameDatabase, } from './session-service.js';
|
|
4
|
+
import { registerCommands, registerGuessMiddleware } from './commands.js';
|
|
5
|
+
import { registerGuessHub } from './hub-register.js';
|
|
6
|
+
const plugin = usePlugin();
|
|
7
|
+
const { logger, useContext, addCron } = plugin;
|
|
8
|
+
registerModels(plugin);
|
|
9
|
+
let services = null;
|
|
10
|
+
useContext('database', (dbFeature) => {
|
|
11
|
+
services = createServices(resolveGameDatabase(dbFeature));
|
|
12
|
+
logger.info(formatCompact({ 模块: '猜数字', 数据模型: '已就绪' }));
|
|
13
|
+
});
|
|
14
|
+
registerGuessHub(() => services);
|
|
15
|
+
registerCommands(plugin, () => services);
|
|
16
|
+
registerGuessMiddleware(plugin, () => services);
|
|
17
|
+
addCron(new Cron('0 */15 * * * *', async () => {
|
|
18
|
+
if (!services)
|
|
19
|
+
return;
|
|
20
|
+
const n = await services.abortStale(30 * 60 * 1000);
|
|
21
|
+
if (n > 0)
|
|
22
|
+
logger.debug(formatCompact({ 猜数字: '清理超时局', count: n }));
|
|
23
|
+
}));
|
|
24
|
+
logger.info(formatCompact({ 模块: '猜数字', 状态: '已加载' }));
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAwB,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,cAAc,EACd,mBAAmB,GAEpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;AAC3B,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;AAE/C,cAAc,CAAC,MAAM,CAAC,CAAC;AAEvB,IAAI,QAAQ,GAA0B,IAAI,CAAC;AAE3C,UAAU,CAAC,UAAU,EAAE,CAAC,SAA0B,EAAE,EAAE;IACpD,QAAQ,GAAG,cAAc,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1D,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,gBAAgB,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AACjC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AACzC,uBAAuB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AAEhD,OAAO,CACL,IAAI,IAAI,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;IACpC,IAAI,CAAC,QAAQ;QAAE,OAAO;IACtB,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,CAAC;QAAE,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACrE,CAAC,CAAC,CACH,CAAC;AAEF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC"}
|
package/lib/models.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Models, Plugin } from 'zhin.js';
|
|
2
|
+
export type GuessSessionStatus = 'active' | 'won' | 'lost' | 'aborted';
|
|
3
|
+
declare module 'zhin.js' {
|
|
4
|
+
interface Models {
|
|
5
|
+
guess_sessions: {
|
|
6
|
+
id: string;
|
|
7
|
+
adapter: string;
|
|
8
|
+
endpoint: string;
|
|
9
|
+
channel_type: string;
|
|
10
|
+
channel_id: string;
|
|
11
|
+
channel_key: string;
|
|
12
|
+
player_id: string;
|
|
13
|
+
player_name: string;
|
|
14
|
+
secret: number;
|
|
15
|
+
range_min: number;
|
|
16
|
+
range_max: number;
|
|
17
|
+
attempts: number;
|
|
18
|
+
max_attempts: number;
|
|
19
|
+
status: GuessSessionStatus;
|
|
20
|
+
updated_at: number;
|
|
21
|
+
created_at: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export type GuessSessionRow = Models['guess_sessions'];
|
|
26
|
+
export declare function registerModels(plugin: Plugin): void;
|
|
27
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAEvE,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,MAAM;QACd,cAAc,EAAE;YACd,EAAE,EAAE,MAAM,CAAC;YACX,OAAO,EAAE,MAAM,CAAC;YAChB,QAAQ,EAAE,MAAM,CAAC;YACjB,YAAY,EAAE,MAAM,CAAC;YACrB,UAAU,EAAE,MAAM,CAAC;YACnB,WAAW,EAAE,MAAM,CAAC;YACpB,SAAS,EAAE,MAAM,CAAC;YAClB,WAAW,EAAE,MAAM,CAAC;YACpB,MAAM,EAAE,MAAM,CAAC;YACf,SAAS,EAAE,MAAM,CAAC;YAClB,SAAS,EAAE,MAAM,CAAC;YAClB,QAAQ,EAAE,MAAM,CAAC;YACjB,YAAY,EAAE,MAAM,CAAC;YACrB,MAAM,EAAE,kBAAkB,CAAC;YAC3B,UAAU,EAAE,MAAM,CAAC;YACnB,UAAU,EAAE,MAAM,CAAC;SACpB,CAAC;KACH;CACF;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAmBnD"}
|
package/lib/models.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function registerModels(plugin) {
|
|
2
|
+
plugin.defineModel('guess_sessions', {
|
|
3
|
+
id: { type: 'text', primary: true },
|
|
4
|
+
adapter: { type: 'text', nullable: false },
|
|
5
|
+
endpoint: { type: 'text', nullable: false },
|
|
6
|
+
channel_type: { type: 'text', nullable: false },
|
|
7
|
+
channel_id: { type: 'text', nullable: false },
|
|
8
|
+
channel_key: { type: 'text', nullable: false },
|
|
9
|
+
player_id: { type: 'text', nullable: false },
|
|
10
|
+
player_name: { type: 'text', default: '' },
|
|
11
|
+
secret: { type: 'integer', nullable: false },
|
|
12
|
+
range_min: { type: 'integer', default: 1 },
|
|
13
|
+
range_max: { type: 'integer', default: 100 },
|
|
14
|
+
attempts: { type: 'integer', default: 0 },
|
|
15
|
+
max_attempts: { type: 'integer', default: 7 },
|
|
16
|
+
status: { type: 'text', default: 'active' },
|
|
17
|
+
updated_at: { type: 'integer', default: 0 },
|
|
18
|
+
created_at: { type: 'integer', default: 0 },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AA6BA,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,CAAC,WAAW,CAAC,gBAAgB,EAAE;QACnC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;QACnC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC1C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC3C,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC/C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC7C,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC9C,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC5C,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;QAC1C,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC5C,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC1C,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE;QAC5C,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QACzC,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC7C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE;QAC3C,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC3C,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;KAC5C,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Database, DatabaseFeature, Message, Models } from 'zhin.js';
|
|
2
|
+
import type { GuessSessionRow } from './models.js';
|
|
3
|
+
export type GuessDatabase = Database<unknown, Models, string>;
|
|
4
|
+
export declare class SessionService {
|
|
5
|
+
private readonly db;
|
|
6
|
+
constructor(db: GuessDatabase);
|
|
7
|
+
getActiveByChannel(channel: string): Promise<GuessSessionRow | null>;
|
|
8
|
+
getActiveForUser(channel: string, userId: string): Promise<GuessSessionRow | null>;
|
|
9
|
+
getById(id: string): Promise<GuessSessionRow | null>;
|
|
10
|
+
createSession(message: Message<any>): Promise<GuessSessionRow>;
|
|
11
|
+
updateSession(id: string, patch: Partial<GuessSessionRow>): Promise<void>;
|
|
12
|
+
abortStale(idleMs: number): Promise<number>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createServices(db: GuessDatabase): SessionService;
|
|
15
|
+
export declare function resolveGameDatabase(feature: DatabaseFeature): GuessDatabase;
|
|
16
|
+
export declare function formatStatus(session: GuessSessionRow): string;
|
|
17
|
+
//# 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;AAGxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAQ9D,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;IASlF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAIpD,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC;IAwB9D,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;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAU7D"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { channelKey, generateSessionId } from '@zhin.js/game-shared';
|
|
2
|
+
import { MAX, MAX_ATTEMPTS, MIN, newSecret } from './engine.js';
|
|
3
|
+
function getModel(db) {
|
|
4
|
+
const model = db.models.get('guess_sessions');
|
|
5
|
+
if (!model)
|
|
6
|
+
throw new Error('guess_sessions not registered');
|
|
7
|
+
return model;
|
|
8
|
+
}
|
|
9
|
+
export class SessionService {
|
|
10
|
+
db;
|
|
11
|
+
constructor(db) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
}
|
|
14
|
+
async getActiveByChannel(channel) {
|
|
15
|
+
const rows = await getModel(this.db).findAll({ channel_key: channel, status: 'active' });
|
|
16
|
+
return rows[0] ?? null;
|
|
17
|
+
}
|
|
18
|
+
async getActiveForUser(channel, userId) {
|
|
19
|
+
const rows = await getModel(this.db).findAll({
|
|
20
|
+
channel_key: channel,
|
|
21
|
+
player_id: userId,
|
|
22
|
+
status: 'active',
|
|
23
|
+
});
|
|
24
|
+
return rows[0] ?? null;
|
|
25
|
+
}
|
|
26
|
+
async getById(id) {
|
|
27
|
+
return getModel(this.db).findOne({ id });
|
|
28
|
+
}
|
|
29
|
+
async createSession(message) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const row = {
|
|
32
|
+
id: generateSessionId(),
|
|
33
|
+
adapter: String(message.$adapter),
|
|
34
|
+
endpoint: message.$endpoint,
|
|
35
|
+
channel_type: message.$channel.type,
|
|
36
|
+
channel_id: message.$channel.id,
|
|
37
|
+
channel_key: channelKey(message),
|
|
38
|
+
player_id: message.$sender.id,
|
|
39
|
+
player_name: message.$sender.name?.trim() || message.$sender.id,
|
|
40
|
+
secret: newSecret(),
|
|
41
|
+
range_min: MIN,
|
|
42
|
+
range_max: MAX,
|
|
43
|
+
attempts: 0,
|
|
44
|
+
max_attempts: MAX_ATTEMPTS,
|
|
45
|
+
status: 'active',
|
|
46
|
+
updated_at: now,
|
|
47
|
+
created_at: now,
|
|
48
|
+
};
|
|
49
|
+
await getModel(this.db).create(row);
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
async updateSession(id, patch) {
|
|
53
|
+
await getModel(this.db).updateWhere({ id }, { ...patch, updated_at: Date.now() });
|
|
54
|
+
}
|
|
55
|
+
async abortStale(idleMs) {
|
|
56
|
+
const cutoff = Date.now() - idleMs;
|
|
57
|
+
const model = getModel(this.db);
|
|
58
|
+
const rows = await model.findAll({ status: 'active' });
|
|
59
|
+
let n = 0;
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
if (row.updated_at < cutoff) {
|
|
62
|
+
await model.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
|
|
63
|
+
n++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function createServices(db) {
|
|
70
|
+
return new SessionService(db);
|
|
71
|
+
}
|
|
72
|
+
export function resolveGameDatabase(feature) {
|
|
73
|
+
return feature.db;
|
|
74
|
+
}
|
|
75
|
+
export function formatStatus(session) {
|
|
76
|
+
const left = session.max_attempts - session.attempts;
|
|
77
|
+
return [
|
|
78
|
+
'🔢 **猜数字**',
|
|
79
|
+
'',
|
|
80
|
+
`我想了一个 **${session.range_min} ~ ${session.range_max}** 之间的整数。`,
|
|
81
|
+
`你还有 **${left}** 次机会(已猜 ${session.attempts} 次)。`,
|
|
82
|
+
'',
|
|
83
|
+
'直接回复数字即可,例如:`50`',
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
//# 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/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhin.js/plugin-guess-number",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zhin.js 猜数字 — 1~100 七步之内猜中",
|
|
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
|
+
"@zhin.js/game-shared": "1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^6.0.3",
|
|
31
|
+
"zhin.js": "4.1.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc --build",
|
|
35
|
+
"clean": "rimraf lib"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/plugin.yml
ADDED
package/src/commands.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Message, MessageCommand, type Plugin } from 'zhin.js';
|
|
2
|
+
import { channelKey, normalizeGuessAction, registerGameTextMiddleware } from '@zhin.js/game-shared';
|
|
3
|
+
import { processGuess } from './game-flow.js';
|
|
4
|
+
import { runGuessCommand } from './guess-command.js';
|
|
5
|
+
import type { SessionService } from './session-service.js';
|
|
6
|
+
|
|
7
|
+
function registerPattern(
|
|
8
|
+
plugin: Plugin,
|
|
9
|
+
pattern: string,
|
|
10
|
+
desc: string,
|
|
11
|
+
getServices: () => SessionService | null,
|
|
12
|
+
): void {
|
|
13
|
+
plugin.addCommand(
|
|
14
|
+
new MessageCommand(pattern)
|
|
15
|
+
.desc(desc)
|
|
16
|
+
.action(async (message, result) => {
|
|
17
|
+
const services = getServices();
|
|
18
|
+
if (!services) return '猜数字需要启用 database 配置。';
|
|
19
|
+
const raw = (result.params.action as string | undefined) ?? '';
|
|
20
|
+
return runGuessCommand(services, message, normalizeGuessAction(raw));
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerCommands(plugin: Plugin, getServices: () => SessionService | null): void {
|
|
26
|
+
registerPattern(plugin, 'guess [action:word]', '猜数字(guess)', getServices);
|
|
27
|
+
registerPattern(plugin, '猜数 [action:word]', '猜数字(中文)', getServices);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerGuessMiddleware(plugin: Plugin, getServices: () => SessionService | null): void {
|
|
31
|
+
registerGameTextMiddleware(plugin, async (message, next) => {
|
|
32
|
+
const services = getServices();
|
|
33
|
+
if (!services) return next();
|
|
34
|
+
|
|
35
|
+
const raw = message.$raw?.trim() ?? '';
|
|
36
|
+
const m = /^(\d+)$/.exec(raw);
|
|
37
|
+
if (!m) return next();
|
|
38
|
+
|
|
39
|
+
const ch = channelKey(message);
|
|
40
|
+
const session = await services.getActiveForUser(ch, message.$sender.id);
|
|
41
|
+
if (!session) return next();
|
|
42
|
+
|
|
43
|
+
const reply = await processGuess(services, message, Number(m[1]));
|
|
44
|
+
if (reply) await message.$reply?.(reply);
|
|
45
|
+
}, 'guess-number:text');
|
|
46
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const GUESS_PREFIX = 'guess';
|
|
2
|
+
export const MIN = 1;
|
|
3
|
+
export const MAX = 100;
|
|
4
|
+
export const MAX_ATTEMPTS = 7;
|
|
5
|
+
|
|
6
|
+
export type GuessResult = 'win' | 'low' | 'high' | 'invalid';
|
|
7
|
+
|
|
8
|
+
export function newSecret(): number {
|
|
9
|
+
return MIN + Math.floor(Math.random() * (MAX - MIN + 1));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function evaluateGuess(secret: number, value: number): GuessResult {
|
|
13
|
+
if (!Number.isInteger(value) || value < MIN || value > MAX) return 'invalid';
|
|
14
|
+
if (value === secret) return 'win';
|
|
15
|
+
return value < secret ? 'low' : 'high';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hintText(result: Exclude<GuessResult, 'win' | 'invalid'>, rangeMin: number, rangeMax: number): string {
|
|
19
|
+
if (result === 'low') return `📈 **太小了!** 范围 ${rangeMin} ~ ${rangeMax}`;
|
|
20
|
+
return `📉 **太大了!** 范围 ${rangeMin} ~ ${rangeMax}`;
|
|
21
|
+
}
|
package/src/game-flow.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Message } from 'zhin.js';
|
|
2
|
+
import { evaluateGuess, hintText, MAX, MIN } from './engine.js';
|
|
3
|
+
import type { SessionService } from './session-service.js';
|
|
4
|
+
import { formatStatus } from './session-service.js';
|
|
5
|
+
|
|
6
|
+
export async function startGame(
|
|
7
|
+
services: SessionService,
|
|
8
|
+
message: Message<any>,
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
11
|
+
const mine = await services.getActiveForUser(ch, message.$sender.id);
|
|
12
|
+
if (mine) {
|
|
13
|
+
return `${formatStatus(mine)}\n\n(你已有进行中的局,继续猜数字,或发送「猜数 放弃」)`;
|
|
14
|
+
}
|
|
15
|
+
const session = await services.createSession(message);
|
|
16
|
+
return formatStatus(session);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function processGuess(
|
|
20
|
+
services: SessionService,
|
|
21
|
+
message: Message<any>,
|
|
22
|
+
value: number,
|
|
23
|
+
): Promise<string | null> {
|
|
24
|
+
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
25
|
+
const session = await services.getActiveForUser(ch, message.$sender.id);
|
|
26
|
+
if (!session) return null;
|
|
27
|
+
|
|
28
|
+
const result = evaluateGuess(session.secret, value);
|
|
29
|
+
if (result === 'invalid') {
|
|
30
|
+
return `请输入 ${MIN} ~ ${MAX} 之间的整数。`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const attempts = session.attempts + 1;
|
|
34
|
+
|
|
35
|
+
if (result === 'win') {
|
|
36
|
+
await services.updateSession(session.id, { attempts, status: 'won' });
|
|
37
|
+
return [
|
|
38
|
+
'🎉 **猜对了!**',
|
|
39
|
+
'',
|
|
40
|
+
`答案就是 **${session.secret}**,你用了 ${attempts} 次。`,
|
|
41
|
+
'发送「猜数 开始」再来一局。',
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (attempts >= session.max_attempts) {
|
|
46
|
+
await services.updateSession(session.id, { attempts, status: 'lost' });
|
|
47
|
+
return [
|
|
48
|
+
'💀 **机会用完了!**',
|
|
49
|
+
'',
|
|
50
|
+
`正确答案是 **${session.secret}**。`,
|
|
51
|
+
'发送「猜数 开始」再来一局。',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let rangeMin = session.range_min;
|
|
56
|
+
let rangeMax = session.range_max;
|
|
57
|
+
if (result === 'low') rangeMin = Math.max(rangeMin, value + 1);
|
|
58
|
+
else rangeMax = Math.min(rangeMax, value - 1);
|
|
59
|
+
|
|
60
|
+
await services.updateSession(session.id, { attempts, range_min: rangeMin, range_max: rangeMax });
|
|
61
|
+
|
|
62
|
+
const left = session.max_attempts - attempts;
|
|
63
|
+
return [
|
|
64
|
+
hintText(result, rangeMin, rangeMax),
|
|
65
|
+
'',
|
|
66
|
+
`剩余 **${left}** 次机会,继续猜吧!`,
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Message, Plugin } from 'zhin.js';
|
|
2
|
+
import { channelKey } from '@zhin.js/game-shared';
|
|
3
|
+
import { startGame } from './game-flow.js';
|
|
4
|
+
import { formatStatus, type SessionService } from './session-service.js';
|
|
5
|
+
|
|
6
|
+
export const GUESS_HELP = [
|
|
7
|
+
'🔢 猜数字(1~100,7 次机会)',
|
|
8
|
+
'猜数 / guess — 帮助',
|
|
9
|
+
'猜数 开始 — 新一局',
|
|
10
|
+
'猜数 放弃 — 结束当前局',
|
|
11
|
+
'',
|
|
12
|
+
'进行中直接回复数字即可。',
|
|
13
|
+
].join('\n');
|
|
14
|
+
|
|
15
|
+
export async function runGuessCommand(
|
|
16
|
+
services: SessionService,
|
|
17
|
+
message: Message<any>,
|
|
18
|
+
action: string,
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
const ch = channelKey(message);
|
|
21
|
+
const userId = message.$sender.id;
|
|
22
|
+
|
|
23
|
+
if (!action || action === 'help') {
|
|
24
|
+
const active = await services.getActiveForUser(ch, userId);
|
|
25
|
+
const lines = [GUESS_HELP, ''];
|
|
26
|
+
if (active) {
|
|
27
|
+
lines.push(formatStatus(active));
|
|
28
|
+
} else {
|
|
29
|
+
lines.push('暂无对局,发送「猜数 开始」。');
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (action === 'start') return startGame(services, message);
|
|
35
|
+
|
|
36
|
+
if (action === 'quit') {
|
|
37
|
+
const row = await services.getActiveForUser(ch, userId);
|
|
38
|
+
if (!row) return '你没有进行中的猜数字。';
|
|
39
|
+
await services.updateSession(row.id, { status: 'aborted' });
|
|
40
|
+
return '已放弃本局猜数字。';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `未知子命令:${action}\n\n${GUESS_HELP}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getPlugin } from 'zhin.js';
|
|
2
|
+
import { ensureGameHubService } from '@zhin.js/game-shared';
|
|
3
|
+
import { runGuessCommand, GUESS_HELP } from './guess-command.js';
|
|
4
|
+
import type { SessionService } from './session-service.js';
|
|
5
|
+
|
|
6
|
+
export function registerGuessHub(getServices: () => SessionService | null): () => void {
|
|
7
|
+
const plugin = getPlugin();
|
|
8
|
+
ensureGameHubService(plugin);
|
|
9
|
+
return plugin.registerGame({
|
|
10
|
+
id: 'guess',
|
|
11
|
+
title: '猜数字',
|
|
12
|
+
icon: '🔢',
|
|
13
|
+
description: '1~100 七步猜中神秘数',
|
|
14
|
+
commandPrefix: '猜数',
|
|
15
|
+
quickStart: '开始',
|
|
16
|
+
aliases: ['guess'],
|
|
17
|
+
menus: [
|
|
18
|
+
{ id: 'start', label: '🎮 开始新局', style: 'primary' },
|
|
19
|
+
{ id: 'help', label: '📖 玩法说明' },
|
|
20
|
+
],
|
|
21
|
+
runAction: async (actionId, ctx) => {
|
|
22
|
+
const services = getServices();
|
|
23
|
+
if (!services) return '猜数字需要启用 database 配置。';
|
|
24
|
+
return runGuessCommand(services, ctx.message, actionId);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { GUESS_HELP };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Cron, formatCompact, usePlugin, type DatabaseFeature } from 'zhin.js';
|
|
2
|
+
import { registerModels } from './models.js';
|
|
3
|
+
import {
|
|
4
|
+
createServices,
|
|
5
|
+
resolveGameDatabase,
|
|
6
|
+
type SessionService,
|
|
7
|
+
} from './session-service.js';
|
|
8
|
+
import { registerCommands, registerGuessMiddleware } from './commands.js';
|
|
9
|
+
import { registerGuessHub } from './hub-register.js';
|
|
10
|
+
|
|
11
|
+
const plugin = usePlugin();
|
|
12
|
+
const { logger, useContext, addCron } = plugin;
|
|
13
|
+
|
|
14
|
+
registerModels(plugin);
|
|
15
|
+
|
|
16
|
+
let services: SessionService | null = null;
|
|
17
|
+
|
|
18
|
+
useContext('database', (dbFeature: DatabaseFeature) => {
|
|
19
|
+
services = createServices(resolveGameDatabase(dbFeature));
|
|
20
|
+
logger.info(formatCompact({ 模块: '猜数字', 数据模型: '已就绪' }));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
registerGuessHub(() => services);
|
|
24
|
+
registerCommands(plugin, () => services);
|
|
25
|
+
registerGuessMiddleware(plugin, () => services);
|
|
26
|
+
|
|
27
|
+
addCron(
|
|
28
|
+
new Cron('0 */15 * * * *', async () => {
|
|
29
|
+
if (!services) return;
|
|
30
|
+
const n = await services.abortStale(30 * 60 * 1000);
|
|
31
|
+
if (n > 0) logger.debug(formatCompact({ 猜数字: '清理超时局', count: n }));
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
logger.info(formatCompact({ 模块: '猜数字', 状态: '已加载' }));
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Models, Plugin } from 'zhin.js';
|
|
2
|
+
|
|
3
|
+
export type GuessSessionStatus = 'active' | 'won' | 'lost' | 'aborted';
|
|
4
|
+
|
|
5
|
+
declare module 'zhin.js' {
|
|
6
|
+
interface Models {
|
|
7
|
+
guess_sessions: {
|
|
8
|
+
id: string;
|
|
9
|
+
adapter: string;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
channel_type: string;
|
|
12
|
+
channel_id: string;
|
|
13
|
+
channel_key: string;
|
|
14
|
+
player_id: string;
|
|
15
|
+
player_name: string;
|
|
16
|
+
secret: number;
|
|
17
|
+
range_min: number;
|
|
18
|
+
range_max: number;
|
|
19
|
+
attempts: number;
|
|
20
|
+
max_attempts: number;
|
|
21
|
+
status: GuessSessionStatus;
|
|
22
|
+
updated_at: number;
|
|
23
|
+
created_at: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type GuessSessionRow = Models['guess_sessions'];
|
|
29
|
+
|
|
30
|
+
export function registerModels(plugin: Plugin): void {
|
|
31
|
+
plugin.defineModel('guess_sessions', {
|
|
32
|
+
id: { type: 'text', primary: true },
|
|
33
|
+
adapter: { type: 'text', nullable: false },
|
|
34
|
+
endpoint: { type: 'text', nullable: false },
|
|
35
|
+
channel_type: { type: 'text', nullable: false },
|
|
36
|
+
channel_id: { type: 'text', nullable: false },
|
|
37
|
+
channel_key: { type: 'text', nullable: false },
|
|
38
|
+
player_id: { type: 'text', nullable: false },
|
|
39
|
+
player_name: { type: 'text', default: '' },
|
|
40
|
+
secret: { type: 'integer', nullable: false },
|
|
41
|
+
range_min: { type: 'integer', default: 1 },
|
|
42
|
+
range_max: { type: 'integer', default: 100 },
|
|
43
|
+
attempts: { type: 'integer', default: 0 },
|
|
44
|
+
max_attempts: { type: 'integer', default: 7 },
|
|
45
|
+
status: { type: 'text', default: 'active' },
|
|
46
|
+
updated_at: { type: 'integer', default: 0 },
|
|
47
|
+
created_at: { type: 'integer', default: 0 },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Database, DatabaseFeature, Message, Models, RelatedModel } from 'zhin.js';
|
|
2
|
+
import { channelKey, generateSessionId } from '@zhin.js/game-shared';
|
|
3
|
+
import { MAX, MAX_ATTEMPTS, MIN, newSecret } from './engine.js';
|
|
4
|
+
import type { GuessSessionRow } from './models.js';
|
|
5
|
+
|
|
6
|
+
export type GuessDatabase = Database<unknown, Models, string>;
|
|
7
|
+
|
|
8
|
+
function getModel(db: GuessDatabase) {
|
|
9
|
+
const model = db.models.get('guess_sessions');
|
|
10
|
+
if (!model) throw new Error('guess_sessions not registered');
|
|
11
|
+
return model as RelatedModel<unknown, Models, 'guess_sessions'>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SessionService {
|
|
15
|
+
constructor(private readonly db: GuessDatabase) {}
|
|
16
|
+
|
|
17
|
+
async getActiveByChannel(channel: string): Promise<GuessSessionRow | null> {
|
|
18
|
+
const rows = await getModel(this.db).findAll({ channel_key: channel, status: 'active' });
|
|
19
|
+
return rows[0] ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getActiveForUser(channel: string, userId: string): Promise<GuessSessionRow | null> {
|
|
23
|
+
const rows = await getModel(this.db).findAll({
|
|
24
|
+
channel_key: channel,
|
|
25
|
+
player_id: userId,
|
|
26
|
+
status: 'active',
|
|
27
|
+
});
|
|
28
|
+
return rows[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getById(id: string): Promise<GuessSessionRow | null> {
|
|
32
|
+
return getModel(this.db).findOne({ id });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createSession(message: Message<any>): Promise<GuessSessionRow> {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const row: GuessSessionRow = {
|
|
38
|
+
id: generateSessionId(),
|
|
39
|
+
adapter: String(message.$adapter),
|
|
40
|
+
endpoint: message.$endpoint,
|
|
41
|
+
channel_type: message.$channel.type,
|
|
42
|
+
channel_id: message.$channel.id,
|
|
43
|
+
channel_key: channelKey(message),
|
|
44
|
+
player_id: message.$sender.id,
|
|
45
|
+
player_name: message.$sender.name?.trim() || message.$sender.id,
|
|
46
|
+
secret: newSecret(),
|
|
47
|
+
range_min: MIN,
|
|
48
|
+
range_max: MAX,
|
|
49
|
+
attempts: 0,
|
|
50
|
+
max_attempts: MAX_ATTEMPTS,
|
|
51
|
+
status: 'active',
|
|
52
|
+
updated_at: now,
|
|
53
|
+
created_at: now,
|
|
54
|
+
};
|
|
55
|
+
await getModel(this.db).create(row);
|
|
56
|
+
return row;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async updateSession(id: string, patch: Partial<GuessSessionRow>): Promise<void> {
|
|
60
|
+
await getModel(this.db).updateWhere({ id }, { ...patch, updated_at: Date.now() });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async abortStale(idleMs: number): Promise<number> {
|
|
64
|
+
const cutoff = Date.now() - idleMs;
|
|
65
|
+
const model = getModel(this.db);
|
|
66
|
+
const rows = await model.findAll({ status: 'active' });
|
|
67
|
+
let n = 0;
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
if (row.updated_at < cutoff) {
|
|
70
|
+
await model.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
|
|
71
|
+
n++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return n;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createServices(db: GuessDatabase): SessionService {
|
|
79
|
+
return new SessionService(db);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveGameDatabase(feature: DatabaseFeature): GuessDatabase {
|
|
83
|
+
return feature.db;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatStatus(session: GuessSessionRow): string {
|
|
87
|
+
const left = session.max_attempts - session.attempts;
|
|
88
|
+
return [
|
|
89
|
+
'🔢 **猜数字**',
|
|
90
|
+
'',
|
|
91
|
+
`我想了一个 **${session.range_min} ~ ${session.range_max}** 之间的整数。`,
|
|
92
|
+
`你还有 **${left}** 次机会(已猜 ${session.attempts} 次)。`,
|
|
93
|
+
'',
|
|
94
|
+
'直接回复数字即可,例如:`50`',
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|