@zhin.js/plugin-dice-duel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/commands.d.ts +6 -0
  2. package/lib/commands.d.ts.map +1 -0
  3. package/lib/commands.js +82 -0
  4. package/lib/commands.js.map +1 -0
  5. package/lib/dice-command.d.ts +5 -0
  6. package/lib/dice-command.d.ts.map +1 -0
  7. package/lib/dice-command.js +37 -0
  8. package/lib/dice-command.js.map +1 -0
  9. package/lib/engine.d.ts +7 -0
  10. package/lib/engine.d.ts.map +1 -0
  11. package/lib/engine.js +16 -0
  12. package/lib/engine.js.map +1 -0
  13. package/lib/game-flow.d.ts +14 -0
  14. package/lib/game-flow.d.ts.map +1 -0
  15. package/lib/game-flow.js +96 -0
  16. package/lib/game-flow.js.map +1 -0
  17. package/lib/hub-register.d.ts +5 -0
  18. package/lib/hub-register.d.ts.map +1 -0
  19. package/lib/hub-register.js +26 -0
  20. package/lib/hub-register.js.map +1 -0
  21. package/lib/index.d.ts +2 -0
  22. package/lib/index.d.ts.map +1 -0
  23. package/lib/index.js +26 -0
  24. package/lib/index.js.map +1 -0
  25. package/lib/models.d.ts +28 -0
  26. package/lib/models.d.ts.map +1 -0
  27. package/lib/models.js +22 -0
  28. package/lib/models.js.map +1 -0
  29. package/lib/session-service.d.ts +17 -0
  30. package/lib/session-service.d.ts.map +1 -0
  31. package/lib/session-service.js +86 -0
  32. package/lib/session-service.js.map +1 -0
  33. package/lib/view.d.ts +8 -0
  34. package/lib/view.d.ts.map +1 -0
  35. package/lib/view.js +41 -0
  36. package/lib/view.js.map +1 -0
  37. package/package.json +37 -0
  38. package/plugin.yml +2 -0
  39. package/src/commands.ts +98 -0
  40. package/src/dice-command.ts +45 -0
  41. package/src/engine.ts +17 -0
  42. package/src/game-flow.ts +127 -0
  43. package/src/hub-register.ts +30 -0
  44. package/src/index.ts +36 -0
  45. package/src/models.ts +51 -0
  46. package/src/session-service.ts +92 -0
  47. package/src/view.ts +49 -0
@@ -0,0 +1,6 @@
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 registerInteractive(plugin: Plugin, getServices: () => SessionService | null): void;
5
+ export declare function registerTextFallback(plugin: Plugin, getServices: () => SessionService | null): void;
6
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AASrF,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;AA2BD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAUlG;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,CAyBnG"}
@@ -0,0 +1,82 @@
1
+ import { MessageCommand, getActionFromMessage } from 'zhin.js';
2
+ import { buildChoiceFallbackMap, channelKey, normalizeDiceAction, parseChoicePayload, } from '@zhin.js/game-shared';
3
+ import { DICE_PREFIX, handleChoice } from './game-flow.js';
4
+ import { runDiceCommand } from './dice-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 runDiceCommand(plugin, services, message, normalizeDiceAction(raw));
14
+ }));
15
+ }
16
+ export function registerCommands(plugin, getServices) {
17
+ registerPattern(plugin, 'dice [action:word]', '骰子对决(dice)', getServices);
18
+ registerPattern(plugin, '骰子 [action:word]', '骰子对决(中文)', getServices);
19
+ }
20
+ async function resolveChoice(message, services) {
21
+ const action = getActionFromMessage(message);
22
+ if (!action)
23
+ return null;
24
+ const parsed = parseChoicePayload(action.payload, DICE_PREFIX);
25
+ if (parsed)
26
+ return { sessionId: parsed.sessionId, choiceId: parsed.choiceId };
27
+ const choiceId = action.id || action.payload;
28
+ if (!choiceId)
29
+ return null;
30
+ const ch = channelKey(message);
31
+ const session = await services.getActiveForUser(ch, message.$sender.id)
32
+ ?? (action.sourceMessageId
33
+ ? await services.getActiveByBoardMessageId(action.sourceMessageId)
34
+ : null);
35
+ if (!session || session.channel_key !== ch)
36
+ return null;
37
+ if (!['roll', 'restart'].includes(choiceId))
38
+ return null;
39
+ return { sessionId: session.id, choiceId };
40
+ }
41
+ export function registerInteractive(plugin, getServices) {
42
+ plugin.registerInteractiveHandler(`${DICE_PREFIX}:`, async (message) => {
43
+ const services = getServices();
44
+ if (!services)
45
+ return false;
46
+ const choice = await resolveChoice(message, services);
47
+ if (!choice)
48
+ return false;
49
+ const err = await handleChoice(plugin, services, message, choice.sessionId, choice.choiceId);
50
+ if (err)
51
+ await message.$reply?.(err);
52
+ return true;
53
+ });
54
+ }
55
+ export function registerTextFallback(plugin, getServices) {
56
+ plugin.addMiddleware(async (message, next) => {
57
+ const services = getServices();
58
+ if (!services)
59
+ return next();
60
+ const action = getActionFromMessage(message);
61
+ if (action?.payload.startsWith(`${DICE_PREFIX}:`))
62
+ return next();
63
+ const ch = channelKey(message);
64
+ const session = await services.getActiveForUser(ch, message.$sender.id);
65
+ if (!session || session.status !== 'active')
66
+ return next();
67
+ const raw = message.$raw?.trim() ?? '';
68
+ if (raw !== '1')
69
+ return next();
70
+ const map = buildChoiceFallbackMap(DICE_PREFIX, session.id, [
71
+ { id: 'roll', label: '掷骰' },
72
+ ]);
73
+ const payload = map['1'];
74
+ const parsed = payload ? parseChoicePayload(payload, DICE_PREFIX) : null;
75
+ if (!parsed || parsed.sessionId !== session.id)
76
+ return next();
77
+ const err = await handleChoice(plugin, services, message, session.id, 'roll');
78
+ if (err)
79
+ await message.$reply?.(err);
80
+ });
81
+ }
82
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { Message, Plugin } from 'zhin.js';
2
+ import type { SessionService } from './session-service.js';
3
+ export declare const DICE_HELP: string;
4
+ export declare function runDiceCommand(plugin: Plugin, services: SessionService, message: Message<any>, action: string): Promise<string | undefined>;
5
+ //# sourceMappingURL=dice-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dice-command.d.ts","sourceRoot":"","sources":["../src/dice-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,eAAO,MAAM,SAAS,QAMV,CAAC;AAEb,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0B7B"}
@@ -0,0 +1,37 @@
1
+ import { channelKey } from '@zhin.js/game-shared';
2
+ import { continueGame, startGame } from './game-flow.js';
3
+ export const DICE_HELP = [
4
+ '🎲 骰子对决(三局两胜)',
5
+ '骰子 / dice — 帮助',
6
+ '骰子 开始 — 新对局',
7
+ '骰子 继续 — 刷新界面',
8
+ '骰子 放弃 — 结束对局',
9
+ ].join('\n');
10
+ export async function runDiceCommand(plugin, services, message, action) {
11
+ const ch = channelKey(message);
12
+ const userId = message.$sender.id;
13
+ if (!action || action === 'help') {
14
+ const active = await services.getActiveByChannel(ch);
15
+ const lines = [DICE_HELP, ''];
16
+ if (active) {
17
+ lines.push(`进行中:${active.player_name} · ${active.player_wins}:${active.bot_wins}`);
18
+ }
19
+ else {
20
+ lines.push('暂无对局,发送「骰子 开始」或从游戏大厅进入。');
21
+ }
22
+ return lines.join('\n');
23
+ }
24
+ if (action === 'start')
25
+ return startGame(plugin, services, message);
26
+ if (action === 'continue')
27
+ return continueGame(plugin, 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${DICE_HELP}`;
36
+ }
37
+ //# sourceMappingURL=dice-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dice-command.js","sourceRoot":"","sources":["../src/dice-command.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGzD,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,eAAe;IACf,gBAAgB;IAChB,aAAa;IACb,cAAc;IACd,cAAc;CACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAc,EACd,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,kBAAkB,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,KAAK,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC9B,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,WAAW,MAAM,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrF,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpE,IAAI,MAAM,KAAK,UAAU;QAAE,OAAO,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAE1E,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,cAAc,CAAC;QAChC,MAAM,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAC5D,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO,SAAS,MAAM,OAAO,SAAS,EAAE,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare const DICE_PREFIX = "dice";
2
+ export declare const WIN_TARGET = 2;
3
+ export declare function rollD6(): number;
4
+ export declare function diceEmoji(n: number): string;
5
+ /** 1=玩家胜 2=机器人胜 0=平局 */
6
+ export declare function compareRolls(player: number, bot: number): 0 | 1 | 2;
7
+ //# 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,WAAW,SAAS,CAAC;AAClC,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED,wBAAwB;AACxB,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAGnE"}
package/lib/engine.js ADDED
@@ -0,0 +1,16 @@
1
+ export const DICE_PREFIX = 'dice';
2
+ export const WIN_TARGET = 2; // best of 3 -> first to 2
3
+ export function rollD6() {
4
+ return 1 + Math.floor(Math.random() * 6);
5
+ }
6
+ export function diceEmoji(n) {
7
+ const faces = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
8
+ return faces[n - 1] ?? String(n);
9
+ }
10
+ /** 1=玩家胜 2=机器人胜 0=平局 */
11
+ export function compareRolls(player, bot) {
12
+ if (player === bot)
13
+ return 0;
14
+ return player > bot ? 1 : 2;
15
+ }
16
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC;AAClC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,0BAA0B;AAEvD,MAAM,UAAU,MAAM;IACpB,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAC7C,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,wBAAwB;AACxB,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,GAAW;IACtD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Message, Plugin } from 'zhin.js';
2
+ import type { DiceSessionRow } from './models.js';
3
+ import { DICE_PREFIX } from './engine.js';
4
+ import type { SessionService } from './session-service.js';
5
+ export declare function sendOrEditView(plugin: Plugin, services: SessionService, message: Message<any>, session: DiceSessionRow, lastRound?: {
6
+ player: number;
7
+ bot: number;
8
+ result: 0 | 1 | 2;
9
+ }): Promise<void>;
10
+ export declare function startGame(plugin: Plugin, services: SessionService, message: Message<any>): Promise<string | undefined>;
11
+ export declare function continueGame(plugin: Plugin, services: SessionService, message: Message<any>): Promise<string>;
12
+ export declare function handleChoice(plugin: Plugin, services: SessionService, message: Message<any>, sessionId: string, choiceId: string): Promise<string | null>;
13
+ export { DICE_PREFIX };
14
+ //# 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,EAAW,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAgB,WAAW,EAAsB,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,OAAO,EAAE,cAAc,EACvB,SAAS,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;CAAE,GAC7D,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GACpB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAY7B;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GACpB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmDxB;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { compareRolls, DICE_PREFIX, rollD6, WIN_TARGET } from './engine.js';
2
+ import { buildDiceView } from './view.js';
3
+ export async function sendOrEditView(plugin, services, message, session, lastRound) {
4
+ const content = buildDiceView(session, lastRound);
5
+ const adapter = plugin.root.inject(message.$adapter);
6
+ if (session.board_message_id) {
7
+ const msgId = await adapter.editMessage({
8
+ messageId: session.board_message_id,
9
+ context: String(message.$adapter),
10
+ endpoint: message.$endpoint,
11
+ id: message.$channel.id,
12
+ type: message.$channel.type,
13
+ content,
14
+ });
15
+ if (msgId !== session.board_message_id) {
16
+ await services.updateSession(session.id, { board_message_id: msgId });
17
+ }
18
+ return;
19
+ }
20
+ const msgId = await message.$reply?.(content);
21
+ if (msgId)
22
+ await services.updateSession(session.id, { board_message_id: msgId });
23
+ }
24
+ export async function startGame(plugin, services, message) {
25
+ const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
26
+ const active = await services.getActiveByChannel(ch);
27
+ if (active) {
28
+ if (active.player_id === message.$sender.id) {
29
+ return '你已有进行中的骰子对决,发送「骰子 继续」刷新。';
30
+ }
31
+ return `本频道 ${active.player_name} 正在掷骰对决中。`;
32
+ }
33
+ const session = await services.createSession(message);
34
+ await sendOrEditView(plugin, services, message, session);
35
+ return undefined;
36
+ }
37
+ export async function continueGame(plugin, services, message) {
38
+ const session = await services.getActiveForUser(`${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`, message.$sender.id);
39
+ if (!session)
40
+ return '你没有进行中的骰子对决,发送「骰子 开始」。';
41
+ await sendOrEditView(plugin, services, message, session);
42
+ return '已刷新骰子界面。';
43
+ }
44
+ export async function handleChoice(plugin, services, message, sessionId, choiceId) {
45
+ const session = await services.getById(sessionId);
46
+ if (!session)
47
+ return '对局不存在。';
48
+ if (session.player_id !== message.$sender.id)
49
+ return '这是别人的对局。';
50
+ if (session.status !== 'active' && choiceId !== 'restart') {
51
+ return '对局已结束,请点击再来一局。';
52
+ }
53
+ if (choiceId === 'restart') {
54
+ await services.updateSession(session.id, {
55
+ player_wins: 0,
56
+ bot_wins: 0,
57
+ round: 0,
58
+ last_player_roll: 0,
59
+ last_bot_roll: 0,
60
+ status: 'active',
61
+ board_message_id: '',
62
+ });
63
+ const updated = (await services.getById(session.id));
64
+ await sendOrEditView(plugin, services, message, updated);
65
+ return null;
66
+ }
67
+ if (choiceId !== 'roll')
68
+ return '无效操作。';
69
+ const player = rollD6();
70
+ const bot = rollD6();
71
+ const result = compareRolls(player, bot);
72
+ let playerWins = session.player_wins;
73
+ let botWins = session.bot_wins;
74
+ if (result === 1)
75
+ playerWins++;
76
+ if (result === 2)
77
+ botWins++;
78
+ let status = 'active';
79
+ if (playerWins >= WIN_TARGET)
80
+ status = 'won';
81
+ else if (botWins >= WIN_TARGET)
82
+ status = 'lost';
83
+ await services.updateSession(session.id, {
84
+ player_wins: playerWins,
85
+ bot_wins: botWins,
86
+ round: session.round + 1,
87
+ last_player_roll: player,
88
+ last_bot_roll: bot,
89
+ status,
90
+ });
91
+ const updated = (await services.getById(session.id));
92
+ await sendOrEditView(plugin, services, message, updated, { player, bot, result });
93
+ return null;
94
+ }
95
+ export { DICE_PREFIX };
96
+ //# 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 { DICE_HELP } from './dice-command.js';
2
+ import type { SessionService } from './session-service.js';
3
+ export declare function registerDiceHub(getServices: () => SessionService | null): () => void;
4
+ export { DICE_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,EAAkB,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,cAAc,GAAG,IAAI,GAAG,MAAM,IAAI,CAoBpF;AAED,OAAO,EAAE,SAAS,EAAE,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { registerGame } from '@zhin.js/game-shared';
2
+ import { runDiceCommand, DICE_HELP } from './dice-command.js';
3
+ export function registerDiceHub(getServices) {
4
+ return registerGame({
5
+ id: 'dice',
6
+ title: '骰子对决',
7
+ icon: '🎲',
8
+ description: '掷骰比大小,三局两胜',
9
+ commandPrefix: '骰子',
10
+ quickStart: '开始',
11
+ aliases: ['dice'],
12
+ menus: [
13
+ { id: 'start', label: '🎮 开始对局', style: 'primary' },
14
+ { id: 'continue', label: '🔄 继续' },
15
+ { id: 'help', label: '📖 玩法说明' },
16
+ ],
17
+ runAction: async (actionId, ctx) => {
18
+ const services = getServices();
19
+ if (!services)
20
+ return '骰子对决需要启用 database 配置。';
21
+ return runDiceCommand(ctx.plugin, services, ctx.message, actionId);
22
+ },
23
+ });
24
+ }
25
+ export { DICE_HELP };
26
+ //# sourceMappingURL=hub-register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hub-register.js","sourceRoot":"","sources":["../src/hub-register.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAG9D,MAAM,UAAU,eAAe,CAAC,WAAwC;IACtE,OAAO,YAAY,CAAC;QAClB,EAAE,EAAE,MAAM;QACV,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,YAAY;QACzB,aAAa,EAAE,IAAI;QACnB,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,CAAC,MAAM,CAAC;QACjB,KAAK,EAAE;YACL,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;YACnD,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE;YAClC,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,uBAAuB,CAAC;YAC9C,OAAO,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACrE,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,SAAS,EAAE,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,26 @@
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, registerInteractive, registerTextFallback } from './commands.js';
5
+ import { registerDiceHub } 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
+ registerDiceHub(() => services);
15
+ registerCommands(plugin, () => services);
16
+ registerInteractive(plugin, () => services);
17
+ registerTextFallback(plugin, () => services);
18
+ addCron(new Cron('0 */15 * * * *', async () => {
19
+ if (!services)
20
+ return;
21
+ const n = await services.abortStale(30 * 60 * 1000);
22
+ if (n > 0)
23
+ logger.debug(formatCompact({ 骰子对决: '清理超时局', count: n }));
24
+ }));
25
+ logger.info(formatCompact({ 模块: '骰子对决', 状态: '已加载' }));
26
+ //# sourceMappingURL=index.js.map
@@ -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,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,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,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,eAAe,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AAChC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AACzC,mBAAmB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AAC5C,oBAAoB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;AAE7C,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,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACtE,CAAC,CAAC,CACH,CAAC;AAEF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Models, Plugin } from 'zhin.js';
2
+ export type DiceSessionStatus = 'active' | 'won' | 'lost' | 'aborted';
3
+ declare module 'zhin.js' {
4
+ interface Models {
5
+ dice_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
+ player_wins: number;
15
+ bot_wins: number;
16
+ round: number;
17
+ last_player_roll: number;
18
+ last_bot_roll: number;
19
+ status: DiceSessionStatus;
20
+ board_message_id: string;
21
+ updated_at: number;
22
+ created_at: number;
23
+ };
24
+ }
25
+ }
26
+ export type DiceSessionRow = Models['dice_sessions'];
27
+ export declare function registerModels(plugin: Plugin): void;
28
+ //# 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,iBAAiB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAEtE,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,MAAM;QACd,aAAa,EAAE;YACb,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,WAAW,EAAE,MAAM,CAAC;YACpB,QAAQ,EAAE,MAAM,CAAC;YACjB,KAAK,EAAE,MAAM,CAAC;YACd,gBAAgB,EAAE,MAAM,CAAC;YACzB,aAAa,EAAE,MAAM,CAAC;YACtB,MAAM,EAAE,iBAAiB,CAAC;YAC1B,gBAAgB,EAAE,MAAM,CAAC;YACzB,UAAU,EAAE,MAAM,CAAC;YACnB,UAAU,EAAE,MAAM,CAAC;SACpB,CAAC;KACH;CACF;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;AAErD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAoBnD"}
package/lib/models.js ADDED
@@ -0,0 +1,22 @@
1
+ export function registerModels(plugin) {
2
+ plugin.defineModel('dice_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
+ player_wins: { type: 'integer', default: 0 },
12
+ bot_wins: { type: 'integer', default: 0 },
13
+ round: { type: 'integer', default: 0 },
14
+ last_player_roll: { type: 'integer', default: 0 },
15
+ last_bot_roll: { type: 'integer', default: 0 },
16
+ status: { type: 'text', default: 'active' },
17
+ board_message_id: { type: 'text', default: '' },
18
+ updated_at: { type: 'integer', default: 0 },
19
+ created_at: { type: 'integer', default: 0 },
20
+ });
21
+ }
22
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AA8BA,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,CAAC,WAAW,CAAC,eAAe,EAAE;QAClC,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,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC5C,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QACzC,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QACtC,gBAAgB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QACjD,aAAa,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC9C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE;QAC3C,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;QAC/C,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 { DiceSessionRow } from './models.js';
3
+ export type DiceDatabase = Database<unknown, Models, string>;
4
+ export declare class SessionService {
5
+ private readonly db;
6
+ constructor(db: DiceDatabase);
7
+ getActiveByChannel(channel: string): Promise<DiceSessionRow | null>;
8
+ getActiveForUser(channel: string, userId: string): Promise<DiceSessionRow | null>;
9
+ getById(id: string): Promise<DiceSessionRow | null>;
10
+ getActiveByBoardMessageId(messageId: string): Promise<DiceSessionRow | null>;
11
+ createSession(message: Message<any>): Promise<DiceSessionRow>;
12
+ updateSession(id: string, patch: Partial<DiceSessionRow>): Promise<void>;
13
+ abortStale(idleMs: number): Promise<number>;
14
+ }
15
+ export declare function createServices(db: DiceDatabase): SessionService;
16
+ export declare function resolveGameDatabase(feature: DatabaseFeature): DiceDatabase;
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;AAExF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAQ7D,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAEvC,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAKnE,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAMjF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAInD,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAW5E,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC;IAyB7D,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxE,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAalD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,cAAc,CAE/D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,GAAG,YAAY,CAE1E"}
@@ -0,0 +1,86 @@
1
+ import { channelKey, generateSessionId } from '@zhin.js/game-shared';
2
+ function getModel(db) {
3
+ const model = db.models.get('dice_sessions');
4
+ if (!model)
5
+ throw new Error('dice_sessions not registered');
6
+ return model;
7
+ }
8
+ export class SessionService {
9
+ db;
10
+ constructor(db) {
11
+ this.db = db;
12
+ }
13
+ async getActiveByChannel(channel) {
14
+ const rows = await getModel(this.db).findAll({ channel_key: channel, status: 'active' });
15
+ return rows[0] ?? null;
16
+ }
17
+ async getActiveForUser(channel, userId) {
18
+ const row = await this.getActiveByChannel(channel);
19
+ if (!row || row.player_id !== userId)
20
+ return null;
21
+ return row;
22
+ }
23
+ async getById(id) {
24
+ return getModel(this.db).findOne({ id });
25
+ }
26
+ async getActiveByBoardMessageId(messageId) {
27
+ if (!messageId)
28
+ return null;
29
+ const rows = await getModel(this.db).findAll({ status: 'active' });
30
+ for (const row of rows) {
31
+ const stored = row.board_message_id;
32
+ if (!stored)
33
+ continue;
34
+ if (stored === messageId || stored.endsWith(`:${messageId}`))
35
+ return row;
36
+ }
37
+ return null;
38
+ }
39
+ async createSession(message) {
40
+ const now = Date.now();
41
+ const row = {
42
+ id: generateSessionId(),
43
+ adapter: String(message.$adapter),
44
+ endpoint: message.$endpoint,
45
+ channel_type: message.$channel.type,
46
+ channel_id: message.$channel.id,
47
+ channel_key: channelKey(message),
48
+ player_id: message.$sender.id,
49
+ player_name: message.$sender.name?.trim() || message.$sender.id,
50
+ player_wins: 0,
51
+ bot_wins: 0,
52
+ round: 0,
53
+ last_player_roll: 0,
54
+ last_bot_roll: 0,
55
+ status: 'active',
56
+ board_message_id: '',
57
+ updated_at: now,
58
+ created_at: now,
59
+ };
60
+ await getModel(this.db).create(row);
61
+ return row;
62
+ }
63
+ async updateSession(id, patch) {
64
+ await getModel(this.db).updateWhere({ id }, { ...patch, updated_at: Date.now() });
65
+ }
66
+ async abortStale(idleMs) {
67
+ const cutoff = Date.now() - idleMs;
68
+ const model = getModel(this.db);
69
+ const rows = await model.findAll({ status: 'active' });
70
+ let n = 0;
71
+ for (const row of rows) {
72
+ if (row.updated_at < cutoff) {
73
+ await model.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
74
+ n++;
75
+ }
76
+ }
77
+ return n;
78
+ }
79
+ }
80
+ export function createServices(db) {
81
+ return new SessionService(db);
82
+ }
83
+ export function resolveGameDatabase(feature) {
84
+ return feature.db;
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/lib/view.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { SendContent } from 'zhin.js';
2
+ import type { DiceSessionRow } from './models.js';
3
+ export declare function buildDiceView(session: DiceSessionRow, lastRound?: {
4
+ player: number;
5
+ bot: number;
6
+ result: 0 | 1 | 2;
7
+ }): SendContent;
8
+ //# 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,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,SAAS,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;CAAE,GAC7D,WAAW,CAwCb"}
package/lib/view.js ADDED
@@ -0,0 +1,41 @@
1
+ import { buildChoiceKeyboard } from '@zhin.js/game-shared';
2
+ import { DICE_PREFIX, diceEmoji } from './engine.js';
3
+ export function buildDiceView(session, lastRound) {
4
+ const terminal = session.status !== 'active';
5
+ const lines = [
6
+ '🎲 **骰子对决**',
7
+ '',
8
+ `比分 · 你 **${session.player_wins}** : **${session.bot_wins}** 机器人(三局两胜)`,
9
+ ];
10
+ if (lastRound) {
11
+ lines.push('');
12
+ lines.push(`你 ${diceEmoji(lastRound.player)}(${lastRound.player}) vs 机器人 ${diceEmoji(lastRound.bot)}(${lastRound.bot})`);
13
+ lines.push(lastRound.result === 0
14
+ ? '🤝 平局,再来!'
15
+ : lastRound.result === 1
16
+ ? '🎉 你赢本局!'
17
+ : '😅 机器人赢本局。');
18
+ }
19
+ if (terminal) {
20
+ lines.push('');
21
+ if (session.status === 'won')
22
+ lines.push('🏆 **你赢得比赛!**');
23
+ else if (session.status === 'lost')
24
+ lines.push('💀 **机器人赢得比赛。**');
25
+ }
26
+ else {
27
+ lines.push('', lastRound ? '点击继续掷骰:' : '点击掷骰开始!');
28
+ }
29
+ const choices = terminal
30
+ ? [{ id: 'restart', label: '🔄 再来一局', style: 'primary' }]
31
+ : [{ id: 'roll', label: '🎲 掷骰', style: 'primary' }];
32
+ return buildChoiceKeyboard({
33
+ gamePrefix: DICE_PREFIX,
34
+ sessionId: session.id,
35
+ narrative: lines.join('\n'),
36
+ choices,
37
+ terminal,
38
+ fallbackHint: '回复 1 掷骰',
39
+ });
40
+ }
41
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../src/view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAc,MAAM,aAAa,CAAC;AAEjE,MAAM,UAAU,aAAa,CAC3B,OAAuB,EACvB,SAA8D;IAE9D,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC;IAC7C,MAAM,KAAK,GAAG;QACZ,aAAa;QACb,EAAE;QACF,YAAY,OAAO,CAAC,WAAW,UAAU,OAAO,CAAC,QAAQ,cAAc;KACxE,CAAC;IAEF,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,MAAM,cAAc,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC;QAC3H,KAAK,CAAC,IAAI,CACR,SAAS,CAAC,MAAM,KAAK,CAAC;YACpB,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;gBACtB,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,YAAY,CACnB,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;aACrD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACpE,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ;QACtB,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAkB,EAAE,CAAC;QAClE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAkB,EAAE,CAAC,CAAC;IAEhE,OAAO,mBAAmB,CAAC;QACzB,UAAU,EAAE,WAAW;QACvB,SAAS,EAAE,OAAO,CAAC,EAAE;QACrB,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QAC3B,OAAO;QACP,QAAQ;QACR,YAAY,EAAE,SAAS;KACxB,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@zhin.js/plugin-dice-duel",
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
+ "@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
@@ -0,0 +1,2 @@
1
+ name: "@zhin.js/plugin-dice-duel"
2
+ description: "骰子对决"
@@ -0,0 +1,98 @@
1
+ import { Message, MessageCommand, getActionFromMessage, type Plugin } from 'zhin.js';
2
+ import {
3
+ buildChoiceFallbackMap,
4
+ channelKey,
5
+ normalizeDiceAction,
6
+ parseChoicePayload,
7
+ registerGameTextMiddleware,
8
+ } from '@zhin.js/game-shared';
9
+ import { DICE_PREFIX, handleChoice } from './game-flow.js';
10
+ import { runDiceCommand } from './dice-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 runDiceCommand(plugin, services, message, normalizeDiceAction(raw));
27
+ }),
28
+ );
29
+ }
30
+
31
+ export function registerCommands(plugin: Plugin, getServices: () => SessionService | null): void {
32
+ registerPattern(plugin, 'dice [action:word]', '骰子对决(dice)', 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, DICE_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
+ if (!['roll', 'restart'].includes(choiceId)) return null;
58
+ return { sessionId: session.id, choiceId };
59
+ }
60
+
61
+ export function registerInteractive(plugin: Plugin, getServices: () => SessionService | null): void {
62
+ plugin.registerInteractiveHandler(`${DICE_PREFIX}:`, async (message) => {
63
+ const services = getServices();
64
+ if (!services) return false;
65
+ const choice = await resolveChoice(message, services);
66
+ if (!choice) return false;
67
+ const err = await handleChoice(plugin, services, message, choice.sessionId, choice.choiceId);
68
+ if (err) await message.$reply?.(err);
69
+ return true;
70
+ });
71
+ }
72
+
73
+ export function registerTextFallback(plugin: Plugin, getServices: () => SessionService | null): void {
74
+ registerGameTextMiddleware(plugin, async (message, next) => {
75
+ const services = getServices();
76
+ if (!services) return next();
77
+
78
+ const action = getActionFromMessage(message);
79
+ if (action?.payload.startsWith(`${DICE_PREFIX}:`)) return next();
80
+
81
+ const ch = channelKey(message);
82
+ const session = await services.getActiveForUser(ch, message.$sender.id);
83
+ if (!session || session.status !== 'active') return next();
84
+
85
+ const raw = message.$raw?.trim() ?? '';
86
+ if (raw !== '1') return next();
87
+
88
+ const map = buildChoiceFallbackMap(DICE_PREFIX, session.id, [
89
+ { id: 'roll', label: '掷骰' },
90
+ ]);
91
+ const payload = map['1'];
92
+ const parsed = payload ? parseChoicePayload(payload, DICE_PREFIX) : null;
93
+ if (!parsed || parsed.sessionId !== session.id) return next();
94
+
95
+ const err = await handleChoice(plugin, services, message, session.id, 'roll');
96
+ if (err) await message.$reply?.(err);
97
+ }, 'dice:text');
98
+ }
@@ -0,0 +1,45 @@
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 type { SessionService } from './session-service.js';
5
+
6
+ export const DICE_HELP = [
7
+ '🎲 骰子对决(三局两胜)',
8
+ '骰子 / dice — 帮助',
9
+ '骰子 开始 — 新对局',
10
+ '骰子 继续 — 刷新界面',
11
+ '骰子 放弃 — 结束对局',
12
+ ].join('\n');
13
+
14
+ export async function runDiceCommand(
15
+ plugin: Plugin,
16
+ services: SessionService,
17
+ message: Message<any>,
18
+ action: string,
19
+ ): Promise<string | undefined> {
20
+ const ch = channelKey(message);
21
+ const userId = message.$sender.id;
22
+
23
+ if (!action || action === 'help') {
24
+ const active = await services.getActiveByChannel(ch);
25
+ const lines = [DICE_HELP, ''];
26
+ if (active) {
27
+ lines.push(`进行中:${active.player_name} · ${active.player_wins}:${active.bot_wins}`);
28
+ } else {
29
+ lines.push('暂无对局,发送「骰子 开始」或从游戏大厅进入。');
30
+ }
31
+ return lines.join('\n');
32
+ }
33
+
34
+ if (action === 'start') return startGame(plugin, services, message);
35
+ if (action === 'continue') return continueGame(plugin, services, message);
36
+
37
+ if (action === 'quit') {
38
+ const row = await services.getActiveForUser(ch, userId);
39
+ if (!row) return '你没有进行中的骰子对决。';
40
+ await services.updateSession(row.id, { status: 'aborted' });
41
+ return '已放弃骰子对局。';
42
+ }
43
+
44
+ return `未知子命令:${action}\n\n${DICE_HELP}`;
45
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,17 @@
1
+ export const DICE_PREFIX = 'dice';
2
+ export const WIN_TARGET = 2; // best of 3 -> first to 2
3
+
4
+ export function rollD6(): number {
5
+ return 1 + Math.floor(Math.random() * 6);
6
+ }
7
+
8
+ export function diceEmoji(n: number): string {
9
+ const faces = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
10
+ return faces[n - 1] ?? String(n);
11
+ }
12
+
13
+ /** 1=玩家胜 2=机器人胜 0=平局 */
14
+ export function compareRolls(player: number, bot: number): 0 | 1 | 2 {
15
+ if (player === bot) return 0;
16
+ return player > bot ? 1 : 2;
17
+ }
@@ -0,0 +1,127 @@
1
+ import type { Adapter, Message, Plugin } from 'zhin.js';
2
+ import type { DiceSessionRow } from './models.js';
3
+ import { compareRolls, DICE_PREFIX, rollD6, WIN_TARGET } from './engine.js';
4
+ import type { SessionService } from './session-service.js';
5
+ import { buildDiceView } from './view.js';
6
+
7
+ export async function sendOrEditView(
8
+ plugin: Plugin,
9
+ services: SessionService,
10
+ message: Message<any>,
11
+ session: DiceSessionRow,
12
+ lastRound?: { player: number; bot: number; result: 0 | 1 | 2 },
13
+ ): Promise<void> {
14
+ const content = buildDiceView(session, lastRound);
15
+ const adapter = plugin.root.inject(message.$adapter) as Adapter;
16
+
17
+ if (session.board_message_id) {
18
+ const msgId = await adapter.editMessage({
19
+ messageId: session.board_message_id,
20
+ context: String(message.$adapter),
21
+ endpoint: message.$endpoint,
22
+ id: message.$channel.id,
23
+ type: message.$channel.type,
24
+ content,
25
+ });
26
+ if (msgId !== session.board_message_id) {
27
+ await services.updateSession(session.id, { board_message_id: msgId });
28
+ }
29
+ return;
30
+ }
31
+
32
+ const msgId = await message.$reply?.(content);
33
+ if (msgId) await services.updateSession(session.id, { board_message_id: msgId });
34
+ }
35
+
36
+ export async function startGame(
37
+ plugin: Plugin,
38
+ services: SessionService,
39
+ message: Message<any>,
40
+ ): Promise<string | undefined> {
41
+ const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
42
+ const active = await services.getActiveByChannel(ch);
43
+ if (active) {
44
+ if (active.player_id === message.$sender.id) {
45
+ return '你已有进行中的骰子对决,发送「骰子 继续」刷新。';
46
+ }
47
+ return `本频道 ${active.player_name} 正在掷骰对决中。`;
48
+ }
49
+ const session = await services.createSession(message);
50
+ await sendOrEditView(plugin, services, message, session);
51
+ return undefined;
52
+ }
53
+
54
+ export async function continueGame(
55
+ plugin: Plugin,
56
+ services: SessionService,
57
+ message: Message<any>,
58
+ ): Promise<string> {
59
+ const session = await services.getActiveForUser(
60
+ `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`,
61
+ message.$sender.id,
62
+ );
63
+ if (!session) return '你没有进行中的骰子对决,发送「骰子 开始」。';
64
+ await sendOrEditView(plugin, services, message, session);
65
+ return '已刷新骰子界面。';
66
+ }
67
+
68
+ export async function handleChoice(
69
+ plugin: Plugin,
70
+ services: SessionService,
71
+ message: Message<any>,
72
+ sessionId: string,
73
+ choiceId: string,
74
+ ): Promise<string | null> {
75
+ const session = await services.getById(sessionId);
76
+ if (!session) return '对局不存在。';
77
+ if (session.player_id !== message.$sender.id) return '这是别人的对局。';
78
+
79
+ if (session.status !== 'active' && choiceId !== 'restart') {
80
+ return '对局已结束,请点击再来一局。';
81
+ }
82
+
83
+ if (choiceId === 'restart') {
84
+ await services.updateSession(session.id, {
85
+ player_wins: 0,
86
+ bot_wins: 0,
87
+ round: 0,
88
+ last_player_roll: 0,
89
+ last_bot_roll: 0,
90
+ status: 'active',
91
+ board_message_id: '',
92
+ });
93
+ const updated = (await services.getById(session.id))!;
94
+ await sendOrEditView(plugin, services, message, updated);
95
+ return null;
96
+ }
97
+
98
+ if (choiceId !== 'roll') return '无效操作。';
99
+
100
+ const player = rollD6();
101
+ const bot = rollD6();
102
+ const result = compareRolls(player, bot);
103
+
104
+ let playerWins = session.player_wins;
105
+ let botWins = session.bot_wins;
106
+ if (result === 1) playerWins++;
107
+ if (result === 2) botWins++;
108
+
109
+ let status: DiceSessionRow['status'] = 'active';
110
+ if (playerWins >= WIN_TARGET) status = 'won';
111
+ else if (botWins >= WIN_TARGET) status = 'lost';
112
+
113
+ await services.updateSession(session.id, {
114
+ player_wins: playerWins,
115
+ bot_wins: botWins,
116
+ round: session.round + 1,
117
+ last_player_roll: player,
118
+ last_bot_roll: bot,
119
+ status,
120
+ });
121
+
122
+ const updated = (await services.getById(session.id))!;
123
+ await sendOrEditView(plugin, services, message, updated, { player, bot, result });
124
+ return null;
125
+ }
126
+
127
+ export { DICE_PREFIX };
@@ -0,0 +1,30 @@
1
+ import { getPlugin } from 'zhin.js';
2
+ import { ensureGameHubService } from '@zhin.js/game-shared';
3
+ import { runDiceCommand, DICE_HELP } from './dice-command.js';
4
+ import type { SessionService } from './session-service.js';
5
+
6
+ export function registerDiceHub(getServices: () => SessionService | null): () => void {
7
+ const plugin = getPlugin();
8
+ ensureGameHubService(plugin);
9
+ return plugin.registerGame({
10
+ id: 'dice',
11
+ title: '骰子对决',
12
+ icon: '🎲',
13
+ description: '掷骰比大小,三局两胜',
14
+ commandPrefix: '骰子',
15
+ quickStart: '开始',
16
+ aliases: ['dice'],
17
+ menus: [
18
+ { id: 'start', label: '🎮 开始对局', style: 'primary' },
19
+ { id: 'continue', label: '🔄 继续' },
20
+ { id: 'help', label: '📖 玩法说明' },
21
+ ],
22
+ runAction: async (actionId, ctx) => {
23
+ const services = getServices();
24
+ if (!services) return '骰子对决需要启用 database 配置。';
25
+ return runDiceCommand(ctx.plugin, services, ctx.message, actionId);
26
+ },
27
+ });
28
+ }
29
+
30
+ export { DICE_HELP };
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
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, registerInteractive, registerTextFallback } from './commands.js';
9
+ import { registerDiceHub } 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
+ registerDiceHub(() => services);
24
+ registerCommands(plugin, () => services);
25
+ registerInteractive(plugin, () => services);
26
+ registerTextFallback(plugin, () => services);
27
+
28
+ addCron(
29
+ new Cron('0 */15 * * * *', async () => {
30
+ if (!services) return;
31
+ const n = await services.abortStale(30 * 60 * 1000);
32
+ if (n > 0) logger.debug(formatCompact({ 骰子对决: '清理超时局', count: n }));
33
+ }),
34
+ );
35
+
36
+ logger.info(formatCompact({ 模块: '骰子对决', 状态: '已加载' }));
package/src/models.ts ADDED
@@ -0,0 +1,51 @@
1
+ import type { Models, Plugin } from 'zhin.js';
2
+
3
+ export type DiceSessionStatus = 'active' | 'won' | 'lost' | 'aborted';
4
+
5
+ declare module 'zhin.js' {
6
+ interface Models {
7
+ dice_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
+ player_wins: number;
17
+ bot_wins: number;
18
+ round: number;
19
+ last_player_roll: number;
20
+ last_bot_roll: number;
21
+ status: DiceSessionStatus;
22
+ board_message_id: string;
23
+ updated_at: number;
24
+ created_at: number;
25
+ };
26
+ }
27
+ }
28
+
29
+ export type DiceSessionRow = Models['dice_sessions'];
30
+
31
+ export function registerModels(plugin: Plugin): void {
32
+ plugin.defineModel('dice_sessions', {
33
+ id: { type: 'text', primary: true },
34
+ adapter: { type: 'text', nullable: false },
35
+ endpoint: { type: 'text', nullable: false },
36
+ channel_type: { type: 'text', nullable: false },
37
+ channel_id: { type: 'text', nullable: false },
38
+ channel_key: { type: 'text', nullable: false },
39
+ player_id: { type: 'text', nullable: false },
40
+ player_name: { type: 'text', default: '' },
41
+ player_wins: { type: 'integer', default: 0 },
42
+ bot_wins: { type: 'integer', default: 0 },
43
+ round: { type: 'integer', default: 0 },
44
+ last_player_roll: { type: 'integer', default: 0 },
45
+ last_bot_roll: { type: 'integer', default: 0 },
46
+ status: { type: 'text', default: 'active' },
47
+ board_message_id: { type: 'text', default: '' },
48
+ updated_at: { type: 'integer', default: 0 },
49
+ created_at: { type: 'integer', default: 0 },
50
+ });
51
+ }
@@ -0,0 +1,92 @@
1
+ import type { Database, DatabaseFeature, Message, Models, RelatedModel } from 'zhin.js';
2
+ import { channelKey, generateSessionId } from '@zhin.js/game-shared';
3
+ import type { DiceSessionRow } from './models.js';
4
+
5
+ export type DiceDatabase = Database<unknown, Models, string>;
6
+
7
+ function getModel(db: DiceDatabase) {
8
+ const model = db.models.get('dice_sessions');
9
+ if (!model) throw new Error('dice_sessions not registered');
10
+ return model as RelatedModel<unknown, Models, 'dice_sessions'>;
11
+ }
12
+
13
+ export class SessionService {
14
+ constructor(private readonly db: DiceDatabase) {}
15
+
16
+ async getActiveByChannel(channel: string): Promise<DiceSessionRow | null> {
17
+ const rows = await getModel(this.db).findAll({ channel_key: channel, status: 'active' });
18
+ return rows[0] ?? null;
19
+ }
20
+
21
+ async getActiveForUser(channel: string, userId: string): Promise<DiceSessionRow | null> {
22
+ const row = await this.getActiveByChannel(channel);
23
+ if (!row || row.player_id !== userId) return null;
24
+ return row;
25
+ }
26
+
27
+ async getById(id: string): Promise<DiceSessionRow | null> {
28
+ return getModel(this.db).findOne({ id });
29
+ }
30
+
31
+ async getActiveByBoardMessageId(messageId: string): Promise<DiceSessionRow | null> {
32
+ if (!messageId) return null;
33
+ const rows = await getModel(this.db).findAll({ status: 'active' });
34
+ for (const row of rows) {
35
+ const stored = row.board_message_id;
36
+ if (!stored) continue;
37
+ if (stored === messageId || stored.endsWith(`:${messageId}`)) return row;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ async createSession(message: Message<any>): Promise<DiceSessionRow> {
43
+ const now = Date.now();
44
+ const row: DiceSessionRow = {
45
+ id: generateSessionId(),
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,
53
+ player_wins: 0,
54
+ bot_wins: 0,
55
+ round: 0,
56
+ last_player_roll: 0,
57
+ last_bot_roll: 0,
58
+ status: 'active',
59
+ board_message_id: '',
60
+ updated_at: now,
61
+ created_at: now,
62
+ };
63
+ await getModel(this.db).create(row);
64
+ return row;
65
+ }
66
+
67
+ async updateSession(id: string, patch: Partial<DiceSessionRow>): Promise<void> {
68
+ await getModel(this.db).updateWhere({ id }, { ...patch, updated_at: Date.now() });
69
+ }
70
+
71
+ async abortStale(idleMs: number): Promise<number> {
72
+ const cutoff = Date.now() - idleMs;
73
+ const model = getModel(this.db);
74
+ const rows = await model.findAll({ status: 'active' });
75
+ let n = 0;
76
+ for (const row of rows) {
77
+ if (row.updated_at < cutoff) {
78
+ await model.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
79
+ n++;
80
+ }
81
+ }
82
+ return n;
83
+ }
84
+ }
85
+
86
+ export function createServices(db: DiceDatabase): SessionService {
87
+ return new SessionService(db);
88
+ }
89
+
90
+ export function resolveGameDatabase(feature: DatabaseFeature): DiceDatabase {
91
+ return feature.db;
92
+ }
package/src/view.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { buildChoiceKeyboard } from '@zhin.js/game-shared';
2
+ import type { SendContent } from 'zhin.js';
3
+ import type { DiceSessionRow } from './models.js';
4
+ import { DICE_PREFIX, diceEmoji, WIN_TARGET } from './engine.js';
5
+
6
+ export function buildDiceView(
7
+ session: DiceSessionRow,
8
+ lastRound?: { player: number; bot: number; result: 0 | 1 | 2 },
9
+ ): SendContent {
10
+ const terminal = session.status !== 'active';
11
+ const lines = [
12
+ '🎲 **骰子对决**',
13
+ '',
14
+ `比分 · 你 **${session.player_wins}** : **${session.bot_wins}** 机器人(三局两胜)`,
15
+ ];
16
+
17
+ if (lastRound) {
18
+ lines.push('');
19
+ lines.push(`你 ${diceEmoji(lastRound.player)}(${lastRound.player}) vs 机器人 ${diceEmoji(lastRound.bot)}(${lastRound.bot})`);
20
+ lines.push(
21
+ lastRound.result === 0
22
+ ? '🤝 平局,再来!'
23
+ : lastRound.result === 1
24
+ ? '🎉 你赢本局!'
25
+ : '😅 机器人赢本局。',
26
+ );
27
+ }
28
+
29
+ if (terminal) {
30
+ lines.push('');
31
+ if (session.status === 'won') lines.push('🏆 **你赢得比赛!**');
32
+ else if (session.status === 'lost') lines.push('💀 **机器人赢得比赛。**');
33
+ } else {
34
+ lines.push('', lastRound ? '点击继续掷骰:' : '点击掷骰开始!');
35
+ }
36
+
37
+ const choices = terminal
38
+ ? [{ id: 'restart', label: '🔄 再来一局', style: 'primary' as const }]
39
+ : [{ id: 'roll', label: '🎲 掷骰', style: 'primary' as const }];
40
+
41
+ return buildChoiceKeyboard({
42
+ gamePrefix: DICE_PREFIX,
43
+ sessionId: session.id,
44
+ narrative: lines.join('\n'),
45
+ choices,
46
+ terminal,
47
+ fallbackHint: '回复 1 掷骰',
48
+ });
49
+ }