@zhin.js/plugin-tic-tac-toe 1.0.1 → 1.0.2

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 (60) hide show
  1. package/commands/ttt/[action:string=].ts +15 -0
  2. package/lib/board-view.d.ts +2 -3
  3. package/lib/board-view.js +1 -1
  4. package/lib/board-view.js.map +1 -1
  5. package/lib/engine.d.ts +0 -1
  6. package/lib/game-flow.d.ts +6 -7
  7. package/lib/game-flow.js +24 -14
  8. package/lib/game-flow.js.map +1 -1
  9. package/lib/index.d.ts +8 -2
  10. package/lib/index.js +6 -34
  11. package/lib/index.js.map +1 -1
  12. package/lib/memory-db.d.ts +6 -0
  13. package/lib/memory-db.js +18 -0
  14. package/lib/memory-db.js.map +1 -0
  15. package/lib/models.d.ts +5 -4
  16. package/lib/models.js +5 -5
  17. package/lib/models.js.map +1 -1
  18. package/lib/player-label.d.ts +1 -2
  19. package/lib/runtime-store.d.ts +6 -0
  20. package/lib/runtime-store.js +7 -0
  21. package/lib/runtime-store.js.map +1 -0
  22. package/lib/session-service.d.ts +1 -3
  23. package/lib/session-service.js +1 -4
  24. package/lib/session-service.js.map +1 -1
  25. package/lib/ttt-command.d.ts +4 -3
  26. package/lib/ttt-command.js +8 -3
  27. package/lib/ttt-command.js.map +1 -1
  28. package/middlewares/ttt-alias.ts +18 -0
  29. package/middlewares/ttt-choice.ts +71 -0
  30. package/package.json +36 -18
  31. package/plugin.ts +64 -0
  32. package/schema.json +6 -0
  33. package/src/board-view.ts +2 -2
  34. package/src/game-flow.ts +31 -24
  35. package/src/index.ts +7 -39
  36. package/src/memory-db.ts +22 -0
  37. package/src/models.ts +10 -10
  38. package/src/player-label.ts +1 -1
  39. package/src/runtime-store.ts +9 -0
  40. package/src/session-service.ts +2 -5
  41. package/src/ttt-command.ts +15 -5
  42. package/lib/board-view.d.ts.map +0 -1
  43. package/lib/commands.d.ts +0 -6
  44. package/lib/commands.d.ts.map +0 -1
  45. package/lib/commands.js +0 -129
  46. package/lib/commands.js.map +0 -1
  47. package/lib/engine.d.ts.map +0 -1
  48. package/lib/game-flow.d.ts.map +0 -1
  49. package/lib/hub-register.d.ts +0 -5
  50. package/lib/hub-register.d.ts.map +0 -1
  51. package/lib/hub-register.js +0 -30
  52. package/lib/hub-register.js.map +0 -1
  53. package/lib/index.d.ts.map +0 -1
  54. package/lib/models.d.ts.map +0 -1
  55. package/lib/player-label.d.ts.map +0 -1
  56. package/lib/session-service.d.ts.map +0 -1
  57. package/lib/ttt-command.d.ts.map +0 -1
  58. package/plugin.yml +0 -2
  59. package/src/commands.ts +0 -157
  60. package/src/hub-register.ts +0 -31
@@ -0,0 +1,71 @@
1
+ import { defineMiddleware } from '@zhin.js/middleware';
2
+ import type { Message } from '@zhin.js/core/runtime';
3
+ import {
4
+ channelKey,
5
+ messageFromCommandInput,
6
+ parseChoicePayload,
7
+ resolveGameTextPayload,
8
+ } from '@zhin.js/game-kit';
9
+ import { resolveGameServices } from '../src/runtime-store.js';
10
+ import { buildFallbackMap, TTT_PREFIX, parseTttPayload } from '../src/board-view.js';
11
+ import { parseBoard } from '../src/engine.js';
12
+ import { handleMove, restartFromTerminal } from '../src/game-flow.js';
13
+
14
+ /**
15
+ * 文本入口:按钮 payload(`ttt:session:0-8` 或 `ttt:session:restart`)
16
+ * 与裸数字 fallback(按空格 1-9 编号落子,对应视图『落子:回复数字 1-9(仅空格)』)。
17
+ */
18
+ export default defineMiddleware<Message>({
19
+ target: 'inbound',
20
+ async handle(context, next) {
21
+ const raw = context.input.content?.trim() ?? '';
22
+ if (!raw) {
23
+ await next();
24
+ return;
25
+ }
26
+ const services = resolveGameServices(context);
27
+ const message = messageFromCommandInput(context.input);
28
+ const ch = channelKey(message);
29
+
30
+ // 直接 payload(QQ 指令预填 / Sandbox action→text)
31
+ const payload = resolveGameTextPayload(raw);
32
+ if (payload?.startsWith(`${TTT_PREFIX}:`)) {
33
+ const restart = parseChoicePayload(payload, TTT_PREFIX);
34
+ if (restart?.choiceId === 'restart') {
35
+ const session = await services.session.getById(restart.sessionId);
36
+ if (session?.channel_key === ch) {
37
+ const reply = await restartFromTerminal(null, services, message, restart.sessionId);
38
+ if (reply) await context.input.$reply(reply);
39
+ return;
40
+ }
41
+ }
42
+ const move = parseTttPayload(payload);
43
+ if (move) {
44
+ const session = await services.session.getById(move.sessionId);
45
+ if (session?.channel_key === ch) {
46
+ const reply = await handleMove(null, services, message, move.sessionId, move.cell);
47
+ if (reply) await context.input.$reply(reply);
48
+ return;
49
+ }
50
+ }
51
+ await next();
52
+ return;
53
+ }
54
+
55
+ // 裸数字:按当前棋盘空格编号映射落子
56
+ const session = await services.session.getActiveForUser(ch, message.$sender.id);
57
+ if (!session || session.channel_key !== ch) {
58
+ await next();
59
+ return;
60
+ }
61
+ const map = buildFallbackMap(session.id, parseBoard(session.board));
62
+ const payloadFromDigit = resolveGameTextPayload(raw, map);
63
+ const move = payloadFromDigit ? parseTttPayload(payloadFromDigit) : null;
64
+ if (!move || move.sessionId !== session.id) {
65
+ await next();
66
+ return;
67
+ }
68
+ const reply = await handleMove(null, services, message, session.id, move.cell);
69
+ if (reply) await context.input.$reply(reply);
70
+ },
71
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-tic-tac-toe",
3
- "version": "1.0.1",
4
- "description": "Zhin.js 跨平台井字棋 — 按钮/文本双模式,群聊排队与人机对战",
3
+ "version": "1.0.2",
4
+ "description": "Zhin.js 跨平台井字棋 (Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -14,29 +14,25 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
+ "plugin.ts",
18
+ "schema.json",
19
+ "commands",
20
+ "middlewares",
17
21
  "src",
18
22
  "lib",
19
- "plugin.yml",
20
23
  "README.md"
21
24
  ],
22
- "keywords": [
23
- "zhin",
24
- "zhin.js",
25
- "bot",
26
- "plugin",
27
- "game",
28
- "tic-tac-toe"
29
- ],
30
25
  "license": "MIT",
31
- "peerDependencies": {
32
- "zhin.js": "4.1.2"
33
- },
34
26
  "dependencies": {
35
- "@zhin.js/game-shared": "1.0.1"
27
+ "@zhin.js/command": "1.0.1",
28
+ "@zhin.js/core": "1.3.5",
29
+ "@zhin.js/game-kit": "1.0.2",
30
+ "@zhin.js/middleware": "1.0.1",
31
+ "@zhin.js/plugin-runtime": "1.0.1"
36
32
  },
37
33
  "devDependencies": {
38
34
  "typescript": "^6.0.3",
39
- "zhin.js": "4.1.2"
35
+ "vitest": "^4.1.10"
40
36
  },
41
37
  "repository": {
42
38
  "type": "git",
@@ -47,8 +43,30 @@
47
43
  "access": "public",
48
44
  "registry": "https://registry.npmjs.org"
49
45
  },
46
+ "engines": {
47
+ "node": "^20.19.0 || >=22.12.0"
48
+ },
49
+ "zhin": {
50
+ "protocol": 1,
51
+ "type": "plugin",
52
+ "entry": "./plugin.ts",
53
+ "engine": "^1.0.0",
54
+ "runtime": "trusted",
55
+ "features": [
56
+ {
57
+ "package": "@zhin.js/command",
58
+ "api": "^1.0.0"
59
+ },
60
+ {
61
+ "package": "@zhin.js/middleware",
62
+ "api": "^1.0.0"
63
+ }
64
+ ],
65
+ "plugins": []
66
+ },
50
67
  "scripts": {
51
- "build": "tsc --build",
52
- "clean": "rimraf lib"
68
+ "build": "tsc",
69
+ "clean": "rimraf lib",
70
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/games/tic-tac-toe/tests"
53
71
  }
54
72
  }
package/plugin.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { definePlugin, databaseHostToken, scheduleHostToken } from '@zhin.js/plugin-runtime';
2
+ import {
3
+ registerRuntimeGame,
4
+ initGameRecordHost,
5
+ DEFAULT_GAME_STALE_CRON,
6
+ DEFAULT_GAME_STALE_IDLE_MS,
7
+ } from '@zhin.js/game-kit';
8
+ import { mountTttHostServices, mountTttMemoryServices } from './src/memory-db.js';
9
+ import { gameServicesToken } from './src/runtime-store.js';
10
+ import type { SessionServices } from './src/session-service.js';
11
+
12
+ /**
13
+ * Plugin Runtime:
14
+ * - Commands under `commands/` are authoritative (help + bot/join/…).
15
+ * - DB: prefer databaseHostToken; else in-memory SessionServices.
16
+ * - Choice middleware under `middlewares/` handles ttt grid / restart payloads (Sandbox action→text).
17
+ */
18
+ export default definePlugin({
19
+ name: 'tic-tac-toe',
20
+ metadata: {
21
+ displayName: 'Tic Tac Toe',
22
+ },
23
+ setup(context) {
24
+ let services: SessionServices;
25
+ if (context.resources.has(databaseHostToken)) {
26
+ const host = context.resources.use(databaseHostToken);
27
+ services = mountTttHostServices(host);
28
+ context.lifecycle.add(initGameRecordHost(host));
29
+ } else {
30
+ services = mountTttMemoryServices();
31
+ }
32
+ context.resources.provide(gameServicesToken, services);
33
+ const disposeHub = registerRuntimeGame({
34
+ id: 'ttt',
35
+ title: '井字棋',
36
+ icon: '♟️',
37
+ description: '三子连珠,群聊排队或人机对战',
38
+ commandPrefix: '/井字棋',
39
+ quickStart: '人机',
40
+ aliases: ['ttt'],
41
+ menus: [
42
+ { id: 'bot', label: '🤖 人机对战' },
43
+ { id: 'join', label: '👥 加入排队' },
44
+ { id: 'spectate', label: '👀 观战' },
45
+ { id: 'help', label: '📖 玩法说明' },
46
+ ],
47
+ });
48
+ context.lifecycle.add(disposeHub);
49
+
50
+ if (context.resources.has(scheduleHostToken)) {
51
+ const schedule = context.resources.use(scheduleHostToken);
52
+ const disposeCron = schedule.register({
53
+ id: 'ttt/abort-stale',
54
+ cron: DEFAULT_GAME_STALE_CRON,
55
+ description: 'Abort stale tic-tac-toe sessions',
56
+ async execute() {
57
+ if (!services.session.abortStale) return;
58
+ await services.session.abortStale(DEFAULT_GAME_STALE_IDLE_MS);
59
+ },
60
+ });
61
+ context.lifecycle.add(disposeCron);
62
+ }
63
+ },
64
+ });
package/schema.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {}
6
+ }
package/src/board-view.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Message, SendContent } from 'zhin.js';
1
+ import type { Message, SendContent } from '@zhin.js/core';
2
2
  import {
3
3
  buildGridKeyboard,
4
4
  buildGridFallbackMap,
@@ -6,7 +6,7 @@ import {
6
6
  parseCellButtonId as parseButtonId,
7
7
  channelKey,
8
8
  type GridCell,
9
- } from '@zhin.js/game-shared';
9
+ } from '@zhin.js/game-kit';
10
10
  import {
11
11
  asciiBoard,
12
12
  cellLabel,
package/src/game-flow.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Adapter, Message, Plugin } from 'zhin.js';
2
- import { recordGameOutcome } from '@zhin.js/game-shared';
1
+ import type { Adapter, Message, Plugin } from '@zhin.js/core';
2
+ import { plainTextFromSendContent, recordGameOutcome } from '@zhin.js/game-kit';
3
3
  import type { TttSessionRow } from './models.js';
4
4
  import { buildBoardInteractive } from './board-view.js';
5
5
  import {
@@ -33,7 +33,7 @@ export function isBotSession(session: TttSessionRow): boolean {
33
33
  }
34
34
 
35
35
  export async function sendOrEditBoard(
36
- plugin: Plugin,
36
+ plugin: Plugin | null,
37
37
  services: SessionServices,
38
38
  message: Message<any>,
39
39
  session: TttSessionRow,
@@ -53,6 +53,8 @@ export async function sendOrEditBoard(
53
53
  channelType: message.$channel.type,
54
54
  });
55
55
 
56
+ if (!plugin) return plainTextFromSendContent(content);
57
+
56
58
  const adapter = plugin.root.inject(message.$adapter) as Adapter;
57
59
 
58
60
  if (session.board_message_id) {
@@ -78,7 +80,7 @@ export async function sendOrEditBoard(
78
80
  }
79
81
 
80
82
  export async function restartFromTerminal(
81
- plugin: Plugin,
83
+ plugin: Plugin | null,
82
84
  services: SessionServices,
83
85
  message: Message<any>,
84
86
  sessionId: string,
@@ -92,7 +94,9 @@ export async function restartFromTerminal(
92
94
  if (!mark) return '你不是本局玩家。';
93
95
  await services.session.updateSession(session.id, { status: 'aborted', winner: 0 });
94
96
  const hint = await startBotGame(plugin, services, message);
95
- return hint.includes('已有') ? hint : null;
97
+ if (hint.includes('已有')) return hint;
98
+ // text-only 模式(plugin===null)下 startBotGame 的唯一输出就是返回文本
99
+ return plugin ? null : hint;
96
100
  }
97
101
 
98
102
  function turnCell(session: TttSessionRow): Cell {
@@ -104,7 +108,7 @@ function playerIdForTurn(session: TttSessionRow): string {
104
108
  }
105
109
 
106
110
  export async function handleMove(
107
- plugin: Plugin,
111
+ plugin: Plugin | null,
108
112
  services: SessionServices,
109
113
  message: Message<any>,
110
114
  sessionId: string,
@@ -177,47 +181,46 @@ export async function handleMove(
177
181
 
178
182
  if (win) {
179
183
  const status = formatWinHeadline(updated, win.winner);
180
- await sendOrEditBoard(plugin, services, message, updated, status, true, win.line);
181
- return null;
184
+ const text = await sendOrEditBoard(plugin, services, message, updated, status, true, win.line);
185
+ // text-only 模式(plugin===null)下 sendOrEditBoard 的唯一输出就是返回文本
186
+ return plugin ? null : text;
182
187
  }
183
188
  if (draw) {
184
- await sendOrEditBoard(plugin, services, message, updated, '平局。', true);
185
- return null;
189
+ const text = await sendOrEditBoard(plugin, services, message, updated, '平局。', true);
190
+ return plugin ? null : text;
186
191
  }
187
192
 
188
193
  // 人机:玩家落子后立刻由服务端代下,避免先发「轮到机器人」再发终盘(QQ 被动消息多耗一次)
189
194
  if (isBotSession(updated) && playerIdForTurn(updated) === BOT_ID) {
190
- await runBotMove(plugin, services, message, updated);
191
- return null;
195
+ return runBotMove(plugin, services, message, updated);
192
196
  }
193
197
 
194
198
  const status = formatTurnStatus(updated, moveCount);
195
- await sendOrEditBoard(plugin, services, message, updated, status, false);
196
-
197
- return null;
199
+ const text = await sendOrEditBoard(plugin, services, message, updated, status, false);
200
+ return plugin ? null : text;
198
201
  }
199
202
 
200
203
  async function runBotMove(
201
- plugin: Plugin,
204
+ plugin: Plugin | null,
202
205
  services: SessionServices,
203
206
  message: Message<any>,
204
207
  session: TttSessionRow,
205
- ): Promise<void> {
208
+ ): Promise<string | null> {
206
209
  const board = parseBoard(session.board);
207
210
  const aiMark = session.player_o === BOT_ID ? O : X;
208
211
  const cell = bestMove(board, aiMark);
209
- if (cell < 0) return;
212
+ if (cell < 0) return null;
210
213
 
211
214
  const fakeMessage = {
212
215
  ...message,
213
216
  $sender: { ...message.$sender, id: BOT_ID, name: '机器人' },
214
217
  } as Message<any>;
215
218
 
216
- await handleMove(plugin, services, fakeMessage, session.id, cell);
219
+ return handleMove(plugin, services, fakeMessage, session.id, cell);
217
220
  }
218
221
 
219
222
  export async function startBotGame(
220
- plugin: Plugin,
223
+ plugin: Plugin | null,
221
224
  services: SessionServices,
222
225
  message: Message<any>,
223
226
  ): Promise<string> {
@@ -235,17 +238,20 @@ export async function startBotGame(
235
238
  });
236
239
 
237
240
  const status = `${formatRosterLine(session)} · 你先手 (✕)`;
238
- await sendOrEditBoard(plugin, services, message, session, status, false);
241
+ const boardText = await sendOrEditBoard(plugin, services, message, session, status, false);
242
+ if (!plugin) {
243
+ return `${boardText}\n\n开局成功!回复数字 1–9 落子。`;
244
+ }
239
245
  return '开局成功!点击棋盘或回复数字落子。';
240
246
  }
241
247
 
242
248
  export async function startPvpGame(
243
- plugin: Plugin,
249
+ plugin: Plugin | null,
244
250
  services: SessionServices,
245
251
  message: Message<any>,
246
252
  playerX: TttPlayerRef,
247
253
  playerO: TttPlayerRef,
248
- ): Promise<void> {
254
+ ): Promise<string | void> {
249
255
  const session = await services.session.createSession({
250
256
  message,
251
257
  playerX: playerX.id,
@@ -256,5 +262,6 @@ export async function startPvpGame(
256
262
  });
257
263
  const opener = formatPlayerWithMark(session.player_x, session.player_x_name, '✕');
258
264
  const status = `${formatRosterLine(session)} · 先手:${opener}`;
259
- await sendOrEditBoard(plugin, services, message, session, status, false);
265
+ const boardText = await sendOrEditBoard(plugin, services, message, session, status, false);
266
+ if (!plugin) return boardText;
260
267
  }
package/src/index.ts CHANGED
@@ -1,41 +1,9 @@
1
+ export { TTT_HELP } from './ttt-command.js';
2
+ export { gameServicesToken, resolveGameServices } from './runtime-store.js';
3
+ export { createInMemoryTttDb, mountTttMemoryServices } from './memory-db.js';
4
+
1
5
  /**
2
- * @zhin.js/plugin-tic-tac-toe — 跨平台井字棋
3
- *
4
- * ```yaml
5
- * plugins:
6
- * - "@zhin.js/plugin-tic-tac-toe"
7
- * database:
8
- * dialect: sqlite
9
- * storage: ./data/zhin.db
10
- * ```
6
+ * 已由 plugin.ts 接线:commands/ 命令、middlewares/ 文本与选项中间件、
7
+ * 游戏大厅注册(registerRuntimeGame)、过期会话 cron(scheduleHostToken)、
8
+ * host DatabaseHost(databaseHostToken)。仍未接:interactive 按钮回调。
11
9
  */
12
- import { formatCompact, usePlugin, type DatabaseFeature } from 'zhin.js';
13
- import { registerModels } from './models.js';
14
- import { createServices, resolveGameDatabase, type SessionServices } from './session-service.js';
15
- import { registerCommands, registerInteractive, registerTextFallback } from './commands.js';
16
- import { registerTttHub } from './hub-register.js';
17
-
18
- const plugin = usePlugin();
19
- const { logger, useContext, addSchedule } = plugin;
20
-
21
- registerModels(plugin);
22
-
23
- let services: SessionServices | null = null;
24
-
25
- useContext('database', (dbFeature: DatabaseFeature) => {
26
- services = createServices(resolveGameDatabase(dbFeature));
27
- logger.debug(formatCompact({ 模块: '井字棋', 数据模型: '已就绪' }));
28
- });
29
-
30
- registerTttHub(() => services);
31
- registerCommands(plugin, () => services);
32
- registerInteractive(plugin, () => services);
33
- registerTextFallback(plugin, () => services);
34
-
35
- addSchedule({ kind: 'solar', cron: '0 */10 * * * *' }, async () => {
36
- if (!services) return;
37
- const n = await services.session.abortStale(30 * 60 * 1000);
38
- if (n > 0) logger.debug(formatCompact({ 井字棋: '清理超时局', count: n }));
39
- });
40
-
41
- logger.debug(formatCompact({ 模块: '井字棋', 状态: '已加载' }));
@@ -0,0 +1,22 @@
1
+ import { createHostGameDb, createInMemoryGameDb } from '@zhin.js/game-kit';
2
+ import type { DatabaseHost } from '@zhin.js/plugin-runtime';
3
+ import { defineHostTables } from './models.js';
4
+ import { createServices, type TttDatabase, type SessionServices } from './session-service.js';
5
+
6
+ const TTT_TABLES = ['ttt_sessions', 'ttt_queue', 'ttt_moves', 'ttt_spectators'] as const;
7
+
8
+ /** In-memory ttt tables for Plugin Runtime slice-2 (no DatabaseFeature). */
9
+ export function createInMemoryTttDb(): TttDatabase {
10
+ return createInMemoryGameDb(TTT_TABLES) as unknown as TttDatabase;
11
+ }
12
+
13
+ export function mountTttMemoryServices(): SessionServices {
14
+ const services = createServices(createInMemoryTttDb());
15
+ return services;
16
+ }
17
+
18
+ export function mountTttHostServices(host: DatabaseHost): SessionServices {
19
+ defineHostTables(host);
20
+ const services = createServices(createHostGameDb(host, TTT_TABLES) as unknown as TttDatabase);
21
+ return services;
22
+ }
package/src/models.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { Models, Plugin } from 'zhin.js';
1
+ import type { Models } from '@zhin.js/core';
2
2
 
3
3
  export type TttSessionStatus = 'active' | 'won' | 'draw' | 'aborted';
4
4
 
5
- declare module 'zhin.js' {
5
+ declare module '@zhin.js/core' {
6
6
  interface Models {
7
7
  ttt_sessions: {
8
8
  id: string;
@@ -56,8 +56,11 @@ export type TttSpectatorRow = Models['ttt_spectators'];
56
56
  /** 井字棋插件注册的表名 */
57
57
  export type TttModelName = 'ttt_sessions' | 'ttt_queue' | 'ttt_moves' | 'ttt_spectators';
58
58
 
59
- export function registerModels(plugin: Plugin): void {
60
- plugin.defineModel('ttt_sessions', {
59
+
60
+ export function defineHostTables(
61
+ db: { define: (name: string, definition: Record<string, unknown>) => void },
62
+ ): void {
63
+ db.define('ttt_sessions', {
61
64
  id: { type: 'text', primary: true },
62
65
  adapter: { type: 'text', nullable: false },
63
66
  endpoint: { type: 'text', nullable: false },
@@ -77,16 +80,14 @@ export function registerModels(plugin: Plugin): void {
77
80
  updated_at: { type: 'integer', default: 0 },
78
81
  created_at: { type: 'integer', default: 0 },
79
82
  });
80
-
81
- plugin.defineModel('ttt_queue', {
83
+ db.define('ttt_queue', {
82
84
  id: { type: 'integer', primary: true },
83
85
  channel_key: { type: 'text', nullable: false },
84
86
  user_id: { type: 'text', nullable: false },
85
87
  user_name: { type: 'text', default: '' },
86
88
  joined_at: { type: 'integer', default: 0 },
87
89
  });
88
-
89
- plugin.defineModel('ttt_moves', {
90
+ db.define('ttt_moves', {
90
91
  id: { type: 'integer', primary: true },
91
92
  session_id: { type: 'text', nullable: false },
92
93
  player_id: { type: 'text', nullable: false },
@@ -94,8 +95,7 @@ export function registerModels(plugin: Plugin): void {
94
95
  move_index: { type: 'integer', nullable: false },
95
96
  created_at: { type: 'integer', default: 0 },
96
97
  });
97
-
98
- plugin.defineModel('ttt_spectators', {
98
+ db.define('ttt_spectators', {
99
99
  id: { type: 'integer', primary: true },
100
100
  session_id: { type: 'text', nullable: false },
101
101
  user_id: { type: 'text', nullable: false },
@@ -1,4 +1,4 @@
1
- import type { MessageSender } from 'zhin.js';
1
+ import type { MessageSender } from '@zhin.js/core';
2
2
  import type { TttSessionRow } from './models.js';
3
3
  import { X, O, type Cell, cellLabel } from './engine.js';
4
4
 
@@ -0,0 +1,9 @@
1
+ import { createToken } from '@zhin.js/plugin-runtime';
2
+ import type { SessionServices } from './session-service.js';
3
+
4
+ /** Owner-scoped service consumed by discovered commands and middleware. */
5
+ export const gameServicesToken = createToken<SessionServices>('zhin.game.tic-tac-toe.services');
6
+
7
+ export function resolveGameServices(context: { use<T>(token: typeof gameServicesToken): T }): SessionServices {
8
+ return context.use(gameServicesToken) as SessionServices;
9
+ }
@@ -1,5 +1,5 @@
1
- import type { Database, DatabaseFeature, Message, Models, RelatedModel } from 'zhin.js';
2
- import { channelKey, boardMessageMatches, generateCompactId } from '@zhin.js/game-shared';
1
+ import type { Database, Message, Models, RelatedModel } from '@zhin.js/core';
2
+ import { channelKey, boardMessageMatches, generateCompactId } from '@zhin.js/game-kit';
3
3
  import type { TttModelName, TttSessionRow } from './models.js';
4
4
 
5
5
  /** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
@@ -195,6 +195,3 @@ export function createServices(db: TttDatabase): SessionServices {
195
195
  return { queue: new QueueService(db), session: new SessionService(db) };
196
196
  }
197
197
 
198
- export function resolveGameDatabase(feature: DatabaseFeature): TttDatabase {
199
- return feature.db;
200
- }
@@ -1,5 +1,5 @@
1
- import type { Message, Plugin } from 'zhin.js';
2
- import { channelKey } from '@zhin.js/game-shared';
1
+ import type { Message, Plugin } from '@zhin.js/core';
2
+ import { channelKey } from '@zhin.js/game-kit';
3
3
  import { formatPlayerWithMark, formatRosterLine } from './player-label.js';
4
4
  import { startBotGame, startPvpGame } from './game-flow.js';
5
5
  import type { SessionServices } from './session-service.js';
@@ -15,7 +15,7 @@ export const TTT_HELP = [
15
15
  ].join('\n');
16
16
 
17
17
  export async function runTttCommand(
18
- plugin: Plugin,
18
+ plugin: Plugin | null,
19
19
  services: SessionServices,
20
20
  message: Message<any>,
21
21
  action: string,
@@ -43,8 +43,9 @@ export async function runTttCommand(
43
43
  const pair = await services.queue.tryMatch(ch);
44
44
  if (pair) {
45
45
  const [px, po] = pair;
46
- await startPvpGame(plugin, services, message, px, po);
47
- return `匹配成功!${formatPlayerWithMark(px.id, px.displayName, '✕')} vs ${formatPlayerWithMark(po.id, po.displayName, '○')}`;
46
+ const board = await startPvpGame(plugin, services, message, px, po);
47
+ const matchLine = `匹配成功!${formatPlayerWithMark(px.id, px.displayName, '✕')} vs ${formatPlayerWithMark(po.id, po.displayName, '○')}`;
48
+ return board ? `${matchLine}\n\n${board}` : matchLine;
48
49
  }
49
50
  return `已加入排队(第 ${position} 位),凑满 2 人自动开局。`;
50
51
  }
@@ -74,3 +75,12 @@ export async function runTttCommand(
74
75
 
75
76
  return `未知子命令:${action}\n\n${TTT_HELP}`;
76
77
  }
78
+
79
+ /** Plugin Runtime / smoke: text-only, no Adapter.editMessage. */
80
+ export async function runTttCommandText(
81
+ services: SessionServices,
82
+ message: Message<any>,
83
+ action: string,
84
+ ): Promise<string> {
85
+ return (await runTttCommand(null, services, message, action)) ?? '';
86
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"board-view.d.ts","sourceRoot":"","sources":["../src/board-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAKL,UAAU,EAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,IAAI,EAIV,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,eAAO,MAAM,UAAU,QAAQ,CAAC;AAEhC,OAAO,EAAE,UAAU,EAAE,CAAC;AAwBtB,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,WAAW,CAoBd;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAExF;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAK3F;AAED,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE3C"}
package/lib/commands.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type Plugin } from 'zhin.js';
2
- import type { SessionServices } from './session-service.js';
3
- export declare function registerCommands(plugin: Plugin, getServices: () => SessionServices | null): void;
4
- export declare function registerInteractive(plugin: Plugin, getServices: () => SessionServices | null): void;
5
- export declare function registerTextFallback(plugin: Plugin, getServices: () => SessionServices | null): void;
6
- //# sourceMappingURL=commands.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAarF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAsD5D,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GACxC,IAAI,CAGN;AAyCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAGnG;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAqCpG"}