@zhin.js/game-kit 1.0.1

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/lib/board-sender.d.ts +14 -0
  4. package/lib/board-sender.d.ts.map +1 -0
  5. package/lib/board-sender.js +7 -0
  6. package/lib/board-sender.js.map +1 -0
  7. package/lib/choice-keyboard.d.ts +44 -0
  8. package/lib/choice-keyboard.d.ts.map +1 -0
  9. package/lib/choice-keyboard.js +67 -0
  10. package/lib/choice-keyboard.js.map +1 -0
  11. package/lib/command-alias.d.ts +21 -0
  12. package/lib/command-alias.d.ts.map +1 -0
  13. package/lib/command-alias.js +22 -0
  14. package/lib/command-alias.js.map +1 -0
  15. package/lib/command-help.d.ts +19 -0
  16. package/lib/command-help.d.ts.map +1 -0
  17. package/lib/command-help.js +91 -0
  18. package/lib/command-help.js.map +1 -0
  19. package/lib/command-message.d.ts +25 -0
  20. package/lib/command-message.d.ts.map +1 -0
  21. package/lib/command-message.js +58 -0
  22. package/lib/command-message.js.map +1 -0
  23. package/lib/game-action-alias.d.ts +8 -0
  24. package/lib/game-action-alias.d.ts.map +1 -0
  25. package/lib/game-action-alias.js +117 -0
  26. package/lib/game-action-alias.js.map +1 -0
  27. package/lib/game-commands.d.ts +3 -0
  28. package/lib/game-commands.d.ts.map +1 -0
  29. package/lib/game-commands.js +5 -0
  30. package/lib/game-commands.js.map +1 -0
  31. package/lib/game-hub-feature.d.ts +58 -0
  32. package/lib/game-hub-feature.d.ts.map +1 -0
  33. package/lib/game-hub-feature.js +100 -0
  34. package/lib/game-hub-feature.js.map +1 -0
  35. package/lib/game-hub-flow.d.ts +9 -0
  36. package/lib/game-hub-flow.d.ts.map +1 -0
  37. package/lib/game-hub-flow.js +90 -0
  38. package/lib/game-hub-flow.js.map +1 -0
  39. package/lib/game-hub-menu-context.d.ts +25 -0
  40. package/lib/game-hub-menu-context.d.ts.map +1 -0
  41. package/lib/game-hub-menu-context.js +64 -0
  42. package/lib/game-hub-menu-context.js.map +1 -0
  43. package/lib/game-hub-menu.d.ts +37 -0
  44. package/lib/game-hub-menu.d.ts.map +1 -0
  45. package/lib/game-hub-menu.js +162 -0
  46. package/lib/game-hub-menu.js.map +1 -0
  47. package/lib/game-hub-mount.d.ts +9 -0
  48. package/lib/game-hub-mount.d.ts.map +1 -0
  49. package/lib/game-hub-mount.js +108 -0
  50. package/lib/game-hub-mount.js.map +1 -0
  51. package/lib/game-interactive.d.ts +25 -0
  52. package/lib/game-interactive.d.ts.map +1 -0
  53. package/lib/game-interactive.js +62 -0
  54. package/lib/game-interactive.js.map +1 -0
  55. package/lib/game-middleware.d.ts +6 -0
  56. package/lib/game-middleware.d.ts.map +1 -0
  57. package/lib/game-middleware.js +9 -0
  58. package/lib/game-middleware.js.map +1 -0
  59. package/lib/game-onboarding.d.ts +8 -0
  60. package/lib/game-onboarding.d.ts.map +1 -0
  61. package/lib/game-onboarding.js +66 -0
  62. package/lib/game-onboarding.js.map +1 -0
  63. package/lib/game-records.d.ts +52 -0
  64. package/lib/game-records.d.ts.map +1 -0
  65. package/lib/game-records.js +206 -0
  66. package/lib/game-records.js.map +1 -0
  67. package/lib/game-session.d.ts +64 -0
  68. package/lib/game-session.d.ts.map +1 -0
  69. package/lib/game-session.js +44 -0
  70. package/lib/game-session.js.map +1 -0
  71. package/lib/grid-keyboard.d.ts +67 -0
  72. package/lib/grid-keyboard.d.ts.map +1 -0
  73. package/lib/grid-keyboard.js +104 -0
  74. package/lib/grid-keyboard.js.map +1 -0
  75. package/lib/help-card.d.ts +12 -0
  76. package/lib/help-card.d.ts.map +1 -0
  77. package/lib/help-card.js +129 -0
  78. package/lib/help-card.js.map +1 -0
  79. package/lib/index.d.ts +29 -0
  80. package/lib/index.d.ts.map +1 -0
  81. package/lib/index.js +29 -0
  82. package/lib/index.js.map +1 -0
  83. package/lib/interaction-profiles.d.ts +16 -0
  84. package/lib/interaction-profiles.d.ts.map +1 -0
  85. package/lib/interaction-profiles.js +35 -0
  86. package/lib/interaction-profiles.js.map +1 -0
  87. package/lib/memory-db.d.ts +41 -0
  88. package/lib/memory-db.d.ts.map +1 -0
  89. package/lib/memory-db.js +92 -0
  90. package/lib/memory-db.js.map +1 -0
  91. package/lib/random.d.ts +6 -0
  92. package/lib/random.d.ts.map +1 -0
  93. package/lib/random.js +29 -0
  94. package/lib/random.js.map +1 -0
  95. package/lib/runtime-hub.d.ts +28 -0
  96. package/lib/runtime-hub.d.ts.map +1 -0
  97. package/lib/runtime-hub.js +66 -0
  98. package/lib/runtime-hub.js.map +1 -0
  99. package/lib/send-content-text.d.ts +5 -0
  100. package/lib/send-content-text.d.ts.map +1 -0
  101. package/lib/send-content-text.js +24 -0
  102. package/lib/send-content-text.js.map +1 -0
  103. package/package.json +49 -0
  104. package/src/board-sender.ts +16 -0
  105. package/src/choice-keyboard.ts +117 -0
  106. package/src/command-alias.ts +45 -0
  107. package/src/command-help.ts +117 -0
  108. package/src/command-message.ts +76 -0
  109. package/src/game-action-alias.ts +129 -0
  110. package/src/game-commands.ts +4 -0
  111. package/src/game-hub-feature.ts +151 -0
  112. package/src/game-hub-flow.ts +114 -0
  113. package/src/game-hub-menu-context.ts +94 -0
  114. package/src/game-hub-menu.ts +223 -0
  115. package/src/game-hub-mount.ts +172 -0
  116. package/src/game-interactive.ts +88 -0
  117. package/src/game-middleware.ts +14 -0
  118. package/src/game-onboarding.ts +67 -0
  119. package/src/game-records.ts +287 -0
  120. package/src/game-session.ts +97 -0
  121. package/src/grid-keyboard.ts +180 -0
  122. package/src/help-card.ts +188 -0
  123. package/src/index.ts +29 -0
  124. package/src/interaction-profiles.ts +56 -0
  125. package/src/memory-db.ts +128 -0
  126. package/src/random.ts +32 -0
  127. package/src/runtime-hub.ts +86 -0
  128. package/src/send-content-text.ts +21 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 通用游戏会话接口
3
+ * 定义回合制游戏的基础会话结构
4
+ */
5
+ import type { Message } from '@zhin.js/core';
6
+ import { generateCompactId } from './random.js';
7
+
8
+ /** 会话状态 */
9
+ export type GameStatus = 'active' | 'won' | 'draw' | 'aborted';
10
+
11
+ /** 通用游戏会话接口 */
12
+ export interface GameSession {
13
+ /** 会话 ID */
14
+ id: string;
15
+ /** 适配器名称 */
16
+ adapter: string;
17
+ /** Endpoint 名称 */
18
+ endpoint: string;
19
+ /** 频道类型 */
20
+ channel_type: 'private' | 'group' | 'channel';
21
+ /** 频道 ID */
22
+ channel_id: string;
23
+ /** 频道唯一键 */
24
+ channel_key: string;
25
+ /** 棋盘消息 ID(用于编辑) */
26
+ board_message_id: string;
27
+ /** 当前状态 */
28
+ status: GameStatus;
29
+ /** 更新时间戳 */
30
+ updated_at: number;
31
+ /** 创建时间戳 */
32
+ created_at: number;
33
+ }
34
+
35
+ /** 回合制游戏会话(2 人对战) */
36
+ export interface TurnBasedSession extends GameSession {
37
+ /** 玩家 1 ID */
38
+ player_1: string;
39
+ /** 玩家 2 ID */
40
+ player_2: string;
41
+ /** 玩家 1 显示名 */
42
+ player_1_name: string;
43
+ /** 玩家 2 显示名 */
44
+ player_2_name: string;
45
+ /** 当前回合(1 或 2) */
46
+ turn: 1 | 2;
47
+ /** 胜者(0=无,1=玩家1,2=玩家2) */
48
+ winner: 0 | 1 | 2;
49
+ /** 步数 */
50
+ move_count: number;
51
+ }
52
+
53
+ /** 生成会话 ID */
54
+ export function generateSessionId(): string {
55
+ return generateCompactId('s');
56
+ }
57
+
58
+ /** 获取当前回合玩家 ID */
59
+ export function currentPlayerId(session: TurnBasedSession): string {
60
+ return session.turn === 1 ? session.player_1 : session.player_2;
61
+ }
62
+
63
+ /** 判断用户是否为本局玩家 */
64
+ export function isPlayer(session: TurnBasedSession, userId: string): boolean {
65
+ return session.player_1 === userId || session.player_2 === userId;
66
+ }
67
+
68
+ /** 判断是否轮到该用户 */
69
+ export function isPlayerTurn(session: TurnBasedSession, userId: string): boolean {
70
+ return currentPlayerId(session) === userId;
71
+ }
72
+
73
+ /** 获取玩家编号(1 或 2),非玩家返回 null */
74
+ export function playerNumber(session: TurnBasedSession, userId: string): 1 | 2 | null {
75
+ if (session.player_1 === userId) return 1;
76
+ if (session.player_2 === userId) return 2;
77
+ return null;
78
+ }
79
+
80
+ /** 切换回合 */
81
+ export function nextTurn(current: 1 | 2): 1 | 2 {
82
+ return current === 1 ? 2 : 1;
83
+ }
84
+
85
+ /** 从 Message 提取发送者显示名 */
86
+ export function senderDisplayName(message: Message<any>): string {
87
+ const name = message.$sender.name?.trim();
88
+ return name || message.$sender.id;
89
+ }
90
+
91
+ /** 比对出站 board_message_id 与平台回调 messageId(含 composite id) */
92
+ export function boardMessageMatches(stored: string, messageId: string): boolean {
93
+ if (!stored || !messageId) return false;
94
+ if (stored === messageId || stored.endsWith(`:${messageId}`)) return true;
95
+ const tail = stored.split(':').pop();
96
+ return !!(tail && messageId.endsWith(tail));
97
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * 通用网格按钮键盘构建器
3
+ * 支持井字棋(3×3)、五子棋(15×15)、四子棋(7×6)等
4
+ */
5
+ import { segment, type SendContent } from '@zhin.js/core';
6
+ import { buildChoiceFallbackMap, type ChoiceOption } from './choice-keyboard.js';
7
+
8
+ import { applyInteractionProfile } from './interaction-profiles.js';
9
+
10
+ export interface GridCell<T = unknown> {
11
+ /** 单元格状态(游戏自定义,如 0=空, 1=X, 2=O) */
12
+ state: T;
13
+ /** 按钮显示文本 */
14
+ label: string;
15
+ /** 是否禁用(已落子或终局) */
16
+ disabled: boolean;
17
+ /** 高亮(胜利连线等) */
18
+ highlight?: boolean;
19
+ }
20
+
21
+ export interface GridKeyboardOptions<T = unknown> {
22
+ /** 游戏前缀(如 'ttt', 'gomoku', 'c4') */
23
+ gamePrefix: string;
24
+ /** 会话 ID */
25
+ sessionId: string;
26
+ /** 行数 */
27
+ rows: number;
28
+ /** 列数 */
29
+ cols: number;
30
+ /** 单元格数据,按行优先 [row * cols + col] */
31
+ cells: GridCell<T>[];
32
+ /** 状态行文本 */
33
+ statusLine: string;
34
+ /** 是否终局(全部按钮禁用) */
35
+ terminal?: boolean;
36
+ /** 省略 ASCII 文本棋盘(QQ 等平台) */
37
+ omitAsciiBoard?: boolean;
38
+ /** ASCII 棋盘渲染器(可选,省略则不渲染 ASCII) */
39
+ renderAscii?: (cells: GridCell<T>[], rows: number, cols: number, highlight?: number[]) => string;
40
+ /** 高亮的单元格索引(胜利连线等) */
41
+ highlight?: number[];
42
+ /** fallback 提示文本 */
43
+ fallbackHint?: string;
44
+ /** 终局时在棋盘下方追加的选项(如「再来一局」,不受 terminal 禁用) */
45
+ postChoices?: ChoiceOption[];
46
+ /** postChoices 使用的 profile,默认 terminal */
47
+ postChoicesProfile?: 'terminal';
48
+ /** terminal 私聊 auto-enter */
49
+ channelType?: string;
50
+ }
51
+
52
+ /**
53
+ * 构建 fallback 数字映射(仅可落子的格子)
54
+ */
55
+ export function buildGridFallbackMap(
56
+ gamePrefix: string,
57
+ sessionId: string,
58
+ cells: GridCell[],
59
+ ): Record<string, string> {
60
+ const map: Record<string, string> = {};
61
+ let n = 1;
62
+ for (let i = 0; i < cells.length; i++) {
63
+ if (!cells[i]!.disabled) {
64
+ map[String(n)] = `${gamePrefix}:${sessionId}:${i}`;
65
+ n++;
66
+ }
67
+ }
68
+ return map;
69
+ }
70
+
71
+ /**
72
+ * 构建网格按钮键盘消息内容
73
+ */
74
+ export function buildGridKeyboard<T>(options: GridKeyboardOptions<T>): SendContent {
75
+ const {
76
+ gamePrefix,
77
+ sessionId,
78
+ rows,
79
+ cols,
80
+ cells,
81
+ statusLine,
82
+ terminal,
83
+ omitAsciiBoard,
84
+ renderAscii,
85
+ highlight,
86
+ fallbackHint,
87
+ postChoices,
88
+ postChoicesProfile = 'terminal',
89
+ channelType,
90
+ } = options;
91
+
92
+ const buttonRows: ReturnType<typeof segment.button>[][] = [];
93
+
94
+ for (let r = 0; r < rows; r++) {
95
+ const row: ReturnType<typeof segment.button>[] = [];
96
+ for (let c = 0; c < cols; c++) {
97
+ const i = r * cols + c;
98
+ const cell = cells[i]!;
99
+ row.push(
100
+ segment.button({
101
+ id: `c${i}`,
102
+ label: cell.label,
103
+ payload: `${gamePrefix}:${sessionId}:${i}`,
104
+ disabled: terminal || cell.disabled,
105
+ style: cell.highlight ? 'primary' : 'secondary',
106
+ }),
107
+ );
108
+ }
109
+ buttonRows.push(row);
110
+ }
111
+
112
+ const fallback = buildGridFallbackMap(gamePrefix, sessionId, cells);
113
+
114
+ const textLines = [statusLine];
115
+ if (!omitAsciiBoard && renderAscii) {
116
+ textLines.push('', renderAscii(cells, rows, cols, highlight));
117
+ }
118
+
119
+ if (postChoices?.length) {
120
+ buttonRows.push(
121
+ postChoices.map((choice) => {
122
+ const base = segment.button({
123
+ id: choice.id,
124
+ label: choice.label,
125
+ payload: `${gamePrefix}:${sessionId}:${choice.id}`,
126
+ disabled: choice.disabled ?? false,
127
+ style: choice.style ?? 'primary',
128
+ });
129
+ return segment.button(
130
+ applyInteractionProfile(base.data, {
131
+ profile: postChoicesProfile,
132
+ channelType,
133
+ }),
134
+ );
135
+ }),
136
+ );
137
+ Object.assign(fallback, buildChoiceFallbackMap(gamePrefix, sessionId, postChoices));
138
+ }
139
+
140
+ const parts = [
141
+ segment.text(textLines.join('\n')),
142
+ segment.keyboard(buttonRows, {
143
+ fallback: fallbackHint || Object.keys(fallback).length > 0
144
+ ? {
145
+ hint: fallbackHint ?? '回复数字选择',
146
+ map: fallback,
147
+ }
148
+ : undefined,
149
+ }).toElement(),
150
+ ];
151
+
152
+ return parts;
153
+ }
154
+
155
+ /**
156
+ * 解析网格 payload:`{prefix}:{sessionId}:{cellIndex}`
157
+ */
158
+ export function parseGridPayload(
159
+ payload: string,
160
+ expectedPrefix?: string,
161
+ ): { prefix: string; sessionId: string; cell: number } | null {
162
+ const m = /^([a-z0-9_]+):([^:]+):(\d+)$/i.exec(payload);
163
+ if (!m) return null;
164
+ const prefix = m[1]!;
165
+ if (expectedPrefix && prefix !== expectedPrefix) return null;
166
+ const cell = Number(m[3]);
167
+ if (!Number.isInteger(cell) || cell < 0) return null;
168
+ return { prefix, sessionId: m[2]!, cell };
169
+ }
170
+
171
+ /**
172
+ * 解析按钮 ID 格式:`c{cellIndex}`(QQ 等平台 action 可能只回传 button_id)
173
+ */
174
+ export function parseCellButtonId(value: string): number | null {
175
+ const m = /^c(\d+)$/.exec(value);
176
+ if (!m) return null;
177
+ const cell = Number(m[1]);
178
+ if (!Number.isInteger(cell) || cell < 0) return null;
179
+ return cell;
180
+ }
@@ -0,0 +1,188 @@
1
+ import {
2
+ Card,
3
+ CardCanvas,
4
+ CardHeader,
5
+ DEFAULT_CARD_THEME,
6
+ h,
7
+ KvRow,
8
+ Section,
9
+ TopicItem,
10
+ } from '@zhin.js/satori';
11
+ import type { RegisteredGame } from './game-hub-feature.js';
12
+ import { slashCommandPrefix } from './game-commands.js';
13
+ import {
14
+ collectSupplementaryCommandHelp,
15
+ formatHubOpenCommandLines,
16
+ type CommandHelpSource,
17
+ } from './command-help.js';
18
+
19
+ export const HELP_CARD_CANVAS = DEFAULT_CARD_THEME.canvas;
20
+ export const HELP_CARD_WIDTH = 560;
21
+
22
+ function cardShell(inner: string): string {
23
+ return h(CardCanvas, {
24
+ width: HELP_CARD_WIDTH,
25
+ backgroundColor: HELP_CARD_CANVAS,
26
+ children: h(Card, { children: inner }),
27
+ });
28
+ }
29
+
30
+ function gameTopicSummary(game: RegisteredGame): string {
31
+ const quick = game.quickStart ?? '开始';
32
+ const cmd = `${slashCommandPrefix(game.commandPrefix.replace(/^\//, ''))} ${quick}`;
33
+ const alias = game.aliases?.length ? ` · ${game.aliases.join(' / ')}` : '';
34
+ return `${cmd}${alias} — ${game.description}`;
35
+ }
36
+
37
+ function commandTopicSummary(cmd: CommandHelpSource): string {
38
+ const [label = '', detail = ''] = cmd.helpInfo.desc;
39
+ return detail || label || cmd.pattern;
40
+ }
41
+
42
+ export interface BotHelpCardInput {
43
+ games: readonly RegisteredGame[];
44
+ commands?: readonly CommandHelpSource[];
45
+ channelType?: string;
46
+ }
47
+
48
+ export function buildBotHelpHtml(input: BotHelpCardInput): string {
49
+ const { games, commands, channelType } = input;
50
+ const hubLines = formatHubOpenCommandLines(commands);
51
+ const other = commands?.length
52
+ ? collectSupplementaryCommandHelp(commands, games)
53
+ : [];
54
+
55
+ const parts: string[] = [];
56
+
57
+ parts.push(
58
+ h(CardHeader, {
59
+ title: '游戏大厅',
60
+ subtitle: games.length
61
+ ? `已注册 ${games.length} 款游戏 · 发送 /游戏 打开按钮大厅`
62
+ : '暂无已注册游戏',
63
+ badge: '🎮',
64
+ }),
65
+ );
66
+
67
+ if (hubLines.length) {
68
+ parts.push(
69
+ h(Section, {
70
+ title: '入口',
71
+ children: hubLines.map((line) => {
72
+ const sep = line.indexOf(' — ');
73
+ const pattern = sep >= 0 ? line.slice(0, sep) : line;
74
+ const desc = sep >= 0 ? line.slice(sep + 3) : '';
75
+ return h(KvRow, { label: pattern, value: desc, labelWidth: 72 });
76
+ }),
77
+ }),
78
+ );
79
+ }
80
+
81
+ if (games.length) {
82
+ parts.push(
83
+ h(Section, {
84
+ title: '游戏列表',
85
+ children: games.map((g, i) =>
86
+ h(TopicItem, {
87
+ index: i + 1,
88
+ title: `${g.icon} ${g.title}`,
89
+ summary: gameTopicSummary(g),
90
+ }),
91
+ ),
92
+ }),
93
+ );
94
+ } else {
95
+ parts.push(
96
+ h(Section, {
97
+ title: '提示',
98
+ children: h(TopicItem, {
99
+ index: 1,
100
+ title: '安装任意游戏插件',
101
+ summary: 'registerGame 后会自动挂载大厅命令',
102
+ }),
103
+ }),
104
+ );
105
+ }
106
+
107
+ if (other.length) {
108
+ parts.push(
109
+ h(Section, {
110
+ title: '其他命令',
111
+ children: other.map((cmd, i) =>
112
+ h(TopicItem, {
113
+ index: i + 1,
114
+ title: cmd.pattern,
115
+ summary: commandTopicSummary(cmd),
116
+ }),
117
+ ),
118
+ }),
119
+ );
120
+ }
121
+
122
+ if (channelType === 'group') {
123
+ parts.push(
124
+ h(Section, {
125
+ title: 'QQ 群用法',
126
+ children: [
127
+ h(TopicItem, {
128
+ index: 1,
129
+ title: '公域群需 @ 机器人',
130
+ summary: '发送命令或直接点击消息按钮',
131
+ }),
132
+ h(TopicItem, {
133
+ index: 2,
134
+ title: '/游戏 打开大厅',
135
+ summary: '同群成员均可点击按钮参与',
136
+ }),
137
+ ],
138
+ }),
139
+ );
140
+ }
141
+
142
+ return cardShell(parts.join(''));
143
+ }
144
+
145
+ export function buildHubHelpHtml(
146
+ games: readonly RegisteredGame[],
147
+ commands?: readonly CommandHelpSource[],
148
+ ): string {
149
+ const hubLines = formatHubOpenCommandLines(commands);
150
+ const parts: string[] = [
151
+ h(CardHeader, {
152
+ title: '游戏大厅帮助',
153
+ subtitle: games.length ? `${games.length} 款游戏可选` : '暂无游戏',
154
+ badge: '🎮',
155
+ }),
156
+ ];
157
+
158
+ if (hubLines.length) {
159
+ parts.push(
160
+ h(Section, {
161
+ title: '入口',
162
+ children: hubLines.map((line) => {
163
+ const sep = line.indexOf(' — ');
164
+ const pattern = sep >= 0 ? line.slice(0, sep) : line;
165
+ const desc = sep >= 0 ? line.slice(sep + 3) : '';
166
+ return h(KvRow, { label: pattern, value: desc, labelWidth: 72 });
167
+ }),
168
+ }),
169
+ );
170
+ }
171
+
172
+ if (games.length) {
173
+ parts.push(
174
+ h(Section, {
175
+ title: '游戏',
176
+ children: games.map((g, i) =>
177
+ h(TopicItem, {
178
+ index: i + 1,
179
+ title: `${g.icon} ${g.title}`,
180
+ summary: gameTopicSummary(g),
181
+ }),
182
+ ),
183
+ }),
184
+ );
185
+ }
186
+
187
+ return cardShell(parts.join(''));
188
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @zhin.js/game-kit - 通用游戏工具包
3
+ *
4
+ * 提供网格棋盘、会话管理、消息发送等通用抽象
5
+ */
6
+
7
+ export * from './grid-keyboard.js';
8
+ export * from './choice-keyboard.js';
9
+ export * from './board-sender.js';
10
+ export * from './game-session.js';
11
+ export * from './game-hub-feature.js';
12
+ export * from './game-hub-menu.js';
13
+ export * from './game-hub-menu-context.js';
14
+ export * from './game-hub-flow.js';
15
+ export * from './game-hub-mount.js';
16
+ export * from './game-middleware.js';
17
+ export * from './game-action-alias.js';
18
+ export * from './game-commands.js';
19
+ export * from './game-interactive.js';
20
+ export * from './interaction-profiles.js';
21
+ export * from './game-records.js';
22
+ export * from './game-onboarding.js';
23
+ export * from './command-help.js';
24
+ export * from './command-message.js';
25
+ export * from './memory-db.js';
26
+ export * from './send-content-text.js';
27
+ export * from './runtime-hub.js';
28
+ export * from './command-alias.js';
29
+ export * from './random.js';
@@ -0,0 +1,56 @@
1
+ import type { ButtonCommandOptions, ButtonData, ButtonInteractionMode } from '@zhin.js/core';
2
+
3
+ /** 交互模式预设:见 ADR 0022 / docs/essentials/interactive-segments.md */
4
+ export type InteractionProfile = 'menu' | 'gameplay' | 'terminal';
5
+
6
+ export interface ApplyInteractionProfileOptions {
7
+ profile: InteractionProfile;
8
+ /** 用于 terminal profile:私聊可 auto-enter */
9
+ channelType?: string;
10
+ }
11
+
12
+ function defaultModeForProfile(
13
+ profile: InteractionProfile,
14
+ channelType?: string,
15
+ ): ButtonInteractionMode {
16
+ if (profile === 'gameplay') return 'callback';
17
+ // 群/频道大厅为共享 UI,须 callback,否则仅 opener 有文本 fallback 上下文
18
+ if (profile === 'menu' && channelType && channelType !== 'private') return 'callback';
19
+ return 'command';
20
+ }
21
+
22
+ function defaultCommandForProfile(
23
+ profile: InteractionProfile,
24
+ channelType?: string,
25
+ ): ButtonCommandOptions | undefined {
26
+ if (profile !== 'terminal') return undefined;
27
+ return { enter: channelType === 'private' };
28
+ }
29
+
30
+ /**
31
+ * 按 profile 注入 button.mode(显式 mode 优先)。
32
+ * - menu / terminal → command
33
+ * - gameplay → callback
34
+ */
35
+ export function applyInteractionProfile(
36
+ btn: ButtonData,
37
+ options: ApplyInteractionProfileOptions,
38
+ ): ButtonData {
39
+ if (btn.mode != null) return btn;
40
+ const mode = defaultModeForProfile(options.profile, options.channelType);
41
+ const commandDefaults = defaultCommandForProfile(options.profile, options.channelType);
42
+ return {
43
+ ...btn,
44
+ mode,
45
+ command: commandDefaults
46
+ ? { ...commandDefaults, ...btn.command }
47
+ : btn.command,
48
+ };
49
+ }
50
+
51
+ export function applyInteractionProfileToButtons(
52
+ buttons: ButtonData[],
53
+ options: ApplyInteractionProfileOptions,
54
+ ): ButtonData[] {
55
+ return buttons.map((btn) => applyInteractionProfile(btn, options));
56
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Minimal in-memory models for game SessionService (findAll/findOne/create/updateWhere/deleteWhere).
3
+ * Slice-2 fallback while Plugin Runtime has no DatabaseFeature Resource path.
4
+ */
5
+
6
+ type Row = Record<string, unknown>;
7
+
8
+ export interface InMemoryGameModel {
9
+ findAll: (q?: Record<string, unknown>) => Promise<Row[]>;
10
+ findOne: (q?: Record<string, unknown>) => Promise<Row | null>;
11
+ create: (row: Row) => Promise<Row>;
12
+ updateWhere: (where: Record<string, unknown>, patch: Row) => Promise<number>;
13
+ deleteWhere: (where: Record<string, unknown>) => Promise<number>;
14
+ }
15
+
16
+ export interface InMemoryGameDb {
17
+ models: { get: (name: string) => InMemoryGameModel | undefined };
18
+ }
19
+
20
+ function createMemoryModel(): InMemoryGameModel {
21
+ const rows: Row[] = [];
22
+
23
+ const match = (row: Row, q: Record<string, unknown>) =>
24
+ Object.entries(q).every(([k, v]) => row[k] === v);
25
+
26
+ return {
27
+ findAll: async (q: Record<string, unknown> = {}) => rows.filter((row) => match(row, q)),
28
+ findOne: async (q: Record<string, unknown> = {}) => rows.find((row) => match(row, q)) ?? null,
29
+ create: async (row: Row) => {
30
+ rows.push({ ...row });
31
+ return row;
32
+ },
33
+ updateWhere: async (where: Record<string, unknown>, patch: Row) => {
34
+ let n = 0;
35
+ for (const row of rows) {
36
+ if (match(row, where)) {
37
+ Object.assign(row, patch);
38
+ n += 1;
39
+ }
40
+ }
41
+ return n;
42
+ },
43
+ deleteWhere: async (where: Record<string, unknown>) => {
44
+ let n = 0;
45
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
46
+ if (match(rows[i]!, where)) {
47
+ rows.splice(i, 1);
48
+ n += 1;
49
+ }
50
+ }
51
+ return n;
52
+ },
53
+ };
54
+ }
55
+
56
+ /** Create an in-memory db with the given model table names. */
57
+ export function createInMemoryGameDb(tableNames: readonly string[]): InMemoryGameDb {
58
+ const models = new Map<string, InMemoryGameModel>();
59
+ for (const name of tableNames) {
60
+ models.set(name, createMemoryModel());
61
+ }
62
+ return {
63
+ models: {
64
+ get: (name: string) => models.get(name),
65
+ },
66
+ };
67
+ }
68
+
69
+ /** Minimal DatabaseHost model surface (structural typing; no plugin-runtime dep). */
70
+ export interface HostGameModelSource {
71
+ select(): {
72
+ where(query: Record<string, unknown>): Promise<Record<string, unknown>[]>;
73
+ };
74
+ insert(row: Record<string, unknown>): Promise<unknown>;
75
+ delete(): { where(query: Record<string, unknown>): Promise<unknown> };
76
+ update(patch: Record<string, unknown>): {
77
+ where(query: Record<string, unknown>): Promise<unknown>;
78
+ };
79
+ }
80
+
81
+ export interface HostGameDbSource {
82
+ models: { get(name: string): HostGameModelSource | undefined };
83
+ }
84
+
85
+ function wrapHostModel(model: HostGameModelSource): InMemoryGameModel {
86
+ return {
87
+ findAll: async (q: Record<string, unknown> = {}) => {
88
+ const rows = await model.select().where(q);
89
+ return rows as Row[];
90
+ },
91
+ findOne: async (q: Record<string, unknown> = {}) => {
92
+ const rows = await model.select().where(q);
93
+ return (rows[0] as Row | undefined) ?? null;
94
+ },
95
+ create: async (row: Row) => {
96
+ await model.insert(row);
97
+ return row;
98
+ },
99
+ updateWhere: async (where: Record<string, unknown>, patch: Row) => {
100
+ const matching = await model.select().where(where);
101
+ if (matching.length === 0) return 0;
102
+ await model.update(patch).where(where);
103
+ return matching.length;
104
+ },
105
+ deleteWhere: async (where: Record<string, unknown>) => {
106
+ const matching = await model.select().where(where);
107
+ if (matching.length === 0) return 0;
108
+ await model.delete().where(where);
109
+ return matching.length;
110
+ },
111
+ };
112
+ }
113
+
114
+ /** Adapt DatabaseHost models to SessionService RelatedModel-style API. */
115
+ export function createHostGameDb(
116
+ host: HostGameDbSource,
117
+ tableNames: readonly string[],
118
+ ): InMemoryGameDb {
119
+ return {
120
+ models: {
121
+ get: (name: string) => {
122
+ if (!tableNames.includes(name)) return undefined;
123
+ const raw = host.models.get(name);
124
+ return raw ? wrapHostModel(raw) : undefined;
125
+ },
126
+ },
127
+ };
128
+ }