@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.
- package/commands/ttt/[action:string=].ts +15 -0
- package/lib/board-view.d.ts +2 -3
- package/lib/board-view.js +1 -1
- package/lib/board-view.js.map +1 -1
- package/lib/engine.d.ts +0 -1
- package/lib/game-flow.d.ts +6 -7
- package/lib/game-flow.js +24 -14
- package/lib/game-flow.js.map +1 -1
- package/lib/index.d.ts +8 -2
- package/lib/index.js +6 -34
- package/lib/index.js.map +1 -1
- package/lib/memory-db.d.ts +6 -0
- package/lib/memory-db.js +18 -0
- package/lib/memory-db.js.map +1 -0
- package/lib/models.d.ts +5 -4
- package/lib/models.js +5 -5
- package/lib/models.js.map +1 -1
- package/lib/player-label.d.ts +1 -2
- package/lib/runtime-store.d.ts +6 -0
- package/lib/runtime-store.js +7 -0
- package/lib/runtime-store.js.map +1 -0
- package/lib/session-service.d.ts +1 -3
- package/lib/session-service.js +1 -4
- package/lib/session-service.js.map +1 -1
- package/lib/ttt-command.d.ts +4 -3
- package/lib/ttt-command.js +8 -3
- package/lib/ttt-command.js.map +1 -1
- package/middlewares/ttt-alias.ts +18 -0
- package/middlewares/ttt-choice.ts +71 -0
- package/package.json +36 -18
- package/plugin.ts +64 -0
- package/schema.json +6 -0
- package/src/board-view.ts +2 -2
- package/src/game-flow.ts +31 -24
- package/src/index.ts +7 -39
- package/src/memory-db.ts +22 -0
- package/src/models.ts +10 -10
- package/src/player-label.ts +1 -1
- package/src/runtime-store.ts +9 -0
- package/src/session-service.ts +2 -5
- package/src/ttt-command.ts +15 -5
- package/lib/board-view.d.ts.map +0 -1
- package/lib/commands.d.ts +0 -6
- package/lib/commands.d.ts.map +0 -1
- package/lib/commands.js +0 -129
- package/lib/commands.js.map +0 -1
- package/lib/engine.d.ts.map +0 -1
- package/lib/game-flow.d.ts.map +0 -1
- package/lib/hub-register.d.ts +0 -5
- package/lib/hub-register.d.ts.map +0 -1
- package/lib/hub-register.js +0 -30
- package/lib/hub-register.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/models.d.ts.map +0 -1
- package/lib/player-label.d.ts.map +0 -1
- package/lib/session-service.d.ts.map +0 -1
- package/lib/ttt-command.d.ts.map +0 -1
- package/plugin.yml +0 -2
- package/src/commands.ts +0 -157
- 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.
|
|
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/
|
|
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
|
-
"
|
|
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
|
|
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
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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({ 模块: '井字棋', 状态: '已加载' }));
|
package/src/memory-db.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
60
|
-
|
|
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 },
|
package/src/player-label.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/session-service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Database,
|
|
2
|
-
import { channelKey, boardMessageMatches, generateCompactId } from '@zhin.js/game-
|
|
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
|
-
}
|
package/src/ttt-command.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Message, Plugin } from 'zhin.js';
|
|
2
|
-
import { channelKey } from '@zhin.js/game-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/board-view.d.ts.map
DELETED
|
@@ -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
|
package/lib/commands.d.ts.map
DELETED
|
@@ -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"}
|