@zhin.js/plugin-tic-tac-toe 1.0.2 → 1.0.4
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 +2 -2
- package/lib/game-flow.d.ts +11 -6
- package/lib/game-flow.js +35 -48
- package/lib/game-flow.js.map +1 -1
- package/lib/session-service.d.ts +7 -2
- package/lib/session-service.js +17 -1
- package/lib/session-service.js.map +1 -1
- package/lib/ttt-command.d.ts +2 -4
- package/lib/ttt-command.js +3 -7
- package/lib/ttt-command.js.map +1 -1
- package/middlewares/ttt-alias.ts +6 -3
- package/middlewares/ttt-choice.ts +3 -3
- package/package.json +6 -6
- package/src/board-view.ts +1 -1
- package/src/game-flow.ts +44 -63
- package/src/session-service.ts +22 -4
- package/src/ttt-command.ts +5 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from '@zhin.js/command';
|
|
2
2
|
import { messageFromCommandInput, normalizeTttAction } from '@zhin.js/game-kit';
|
|
3
|
-
import { TTT_HELP,
|
|
3
|
+
import { TTT_HELP, runTttCommand } from '../../src/ttt-command.js';
|
|
4
4
|
import { resolveGameServices } from '../../src/runtime-store.js';
|
|
5
5
|
|
|
6
6
|
export default defineCommand({
|
|
@@ -10,6 +10,6 @@ export default defineCommand({
|
|
|
10
10
|
if (!action || action === 'help') return TTT_HELP;
|
|
11
11
|
const services = resolveGameServices({ use, owner });
|
|
12
12
|
const message = messageFromCommandInput(input);
|
|
13
|
-
return
|
|
13
|
+
return runTttCommand(services, message, action);
|
|
14
14
|
},
|
|
15
15
|
});
|
package/lib/game-flow.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type GameMessageLike } from '@zhin.js/game-kit';
|
|
2
2
|
import type { TttSessionRow } from './models.js';
|
|
3
3
|
import { BOT_ID } from './player-label.js';
|
|
4
4
|
import type { SessionServices, TttPlayerRef } from './session-service.js';
|
|
5
5
|
export { BOT_ID };
|
|
6
6
|
export declare function isBotSession(session: TttSessionRow): boolean;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Plugin Runtime: render the board as text. Interactive in-place board editing
|
|
9
|
+
* (the old Adapter.editMessage path) is not part of the runtime flow; commands
|
|
10
|
+
* and the choice middleware return fresh text each turn.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sendOrEditBoard(message: GameMessageLike, session: TttSessionRow, statusLine: string, terminal?: boolean, highlight?: number[]): string;
|
|
13
|
+
export declare function restartFromTerminal(services: SessionServices, message: GameMessageLike, sessionId: string): Promise<string | null>;
|
|
14
|
+
export declare function handleMove(services: SessionServices, message: GameMessageLike, sessionId: string, cell: number): Promise<string | null>;
|
|
15
|
+
export declare function startBotGame(services: SessionServices, message: GameMessageLike): Promise<string>;
|
|
16
|
+
export declare function startPvpGame(services: SessionServices, message: GameMessageLike, playerX: TttPlayerRef, playerO: TttPlayerRef): Promise<string>;
|
package/lib/game-flow.js
CHANGED
|
@@ -6,7 +6,12 @@ export { BOT_ID };
|
|
|
6
6
|
export function isBotSession(session) {
|
|
7
7
|
return session.player_o === BOT_ID || session.player_x === BOT_ID;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Plugin Runtime: render the board as text. Interactive in-place board editing
|
|
11
|
+
* (the old Adapter.editMessage path) is not part of the runtime flow; commands
|
|
12
|
+
* and the choice middleware return fresh text each turn.
|
|
13
|
+
*/
|
|
14
|
+
export function sendOrEditBoard(message, session, statusLine, terminal = false, highlight) {
|
|
10
15
|
const board = parseBoard(session.board);
|
|
11
16
|
const content = buildBoardInteractive({
|
|
12
17
|
sessionId: session.id,
|
|
@@ -18,33 +23,14 @@ export async function sendOrEditBoard(plugin, services, message, session, status
|
|
|
18
23
|
highlight,
|
|
19
24
|
channelType: message.$channel.type,
|
|
20
25
|
});
|
|
21
|
-
|
|
22
|
-
return plainTextFromSendContent(content);
|
|
23
|
-
const adapter = plugin.root.inject(message.$adapter);
|
|
24
|
-
if (session.board_message_id) {
|
|
25
|
-
const msgId = await adapter.editMessage({
|
|
26
|
-
messageId: session.board_message_id,
|
|
27
|
-
context: String(message.$adapter),
|
|
28
|
-
endpoint: message.$endpoint,
|
|
29
|
-
id: message.$channel.id,
|
|
30
|
-
type: message.$channel.type,
|
|
31
|
-
content,
|
|
32
|
-
});
|
|
33
|
-
if (msgId !== session.board_message_id) {
|
|
34
|
-
await services.session.updateSession(session.id, { board_message_id: msgId });
|
|
35
|
-
}
|
|
36
|
-
return msgId;
|
|
37
|
-
}
|
|
38
|
-
const msgId = await message.$reply?.(content);
|
|
39
|
-
if (msgId) {
|
|
40
|
-
await services.session.updateSession(session.id, { board_message_id: msgId });
|
|
41
|
-
}
|
|
42
|
-
return msgId ?? '';
|
|
26
|
+
return plainTextFromSendContent(content);
|
|
43
27
|
}
|
|
44
|
-
export async function restartFromTerminal(
|
|
28
|
+
export async function restartFromTerminal(services, message, sessionId) {
|
|
45
29
|
const session = await services.session.getById(sessionId);
|
|
46
30
|
if (!session)
|
|
47
31
|
return '对局不存在。';
|
|
32
|
+
if (session.status === 'active')
|
|
33
|
+
return '对局尚未结束,无法重开。';
|
|
48
34
|
const mark = playerMark(message.$sender.id, {
|
|
49
35
|
playerX: session.player_x,
|
|
50
36
|
playerO: session.player_o,
|
|
@@ -52,11 +38,17 @@ export async function restartFromTerminal(plugin, services, message, sessionId)
|
|
|
52
38
|
if (!mark)
|
|
53
39
|
return '你不是本局玩家。';
|
|
54
40
|
await services.session.updateSession(session.id, { status: 'aborted', winner: 0 });
|
|
55
|
-
|
|
41
|
+
// PvP 局重开仍是 PvP(原班人马),不静默降级为人机
|
|
42
|
+
if (!isBotSession(session)) {
|
|
43
|
+
const board = await startPvpGame(services, message, { id: session.player_x, displayName: session.player_x_name }, { id: session.player_o, displayName: session.player_o_name });
|
|
44
|
+
if (board.includes('已有'))
|
|
45
|
+
return board;
|
|
46
|
+
return board;
|
|
47
|
+
}
|
|
48
|
+
const hint = await startBotGame(services, message);
|
|
56
49
|
if (hint.includes('已有'))
|
|
57
50
|
return hint;
|
|
58
|
-
|
|
59
|
-
return plugin ? null : hint;
|
|
51
|
+
return hint;
|
|
60
52
|
}
|
|
61
53
|
function turnCell(session) {
|
|
62
54
|
return session.turn === 1 ? X : O;
|
|
@@ -64,7 +56,7 @@ function turnCell(session) {
|
|
|
64
56
|
function playerIdForTurn(session) {
|
|
65
57
|
return session.turn === 1 ? session.player_x : session.player_o;
|
|
66
58
|
}
|
|
67
|
-
export async function handleMove(
|
|
59
|
+
export async function handleMove(services, message, sessionId, cell) {
|
|
68
60
|
const session = await services.session.getById(sessionId);
|
|
69
61
|
if (!session || session.status !== 'active') {
|
|
70
62
|
return '对局不存在或已结束。';
|
|
@@ -126,23 +118,19 @@ export async function handleMove(plugin, services, message, sessionId, cell) {
|
|
|
126
118
|
}
|
|
127
119
|
if (win) {
|
|
128
120
|
const status = formatWinHeadline(updated, win.winner);
|
|
129
|
-
|
|
130
|
-
// text-only 模式(plugin===null)下 sendOrEditBoard 的唯一输出就是返回文本
|
|
131
|
-
return plugin ? null : text;
|
|
121
|
+
return sendOrEditBoard(message, updated, status, true, win.line);
|
|
132
122
|
}
|
|
133
123
|
if (draw) {
|
|
134
|
-
|
|
135
|
-
return plugin ? null : text;
|
|
124
|
+
return sendOrEditBoard(message, updated, '平局。', true);
|
|
136
125
|
}
|
|
137
126
|
// 人机:玩家落子后立刻由服务端代下,避免先发「轮到机器人」再发终盘(QQ 被动消息多耗一次)
|
|
138
127
|
if (isBotSession(updated) && playerIdForTurn(updated) === BOT_ID) {
|
|
139
|
-
return runBotMove(
|
|
128
|
+
return runBotMove(services, message, updated);
|
|
140
129
|
}
|
|
141
130
|
const status = formatTurnStatus(updated, moveCount);
|
|
142
|
-
|
|
143
|
-
return plugin ? null : text;
|
|
131
|
+
return sendOrEditBoard(message, updated, status, false);
|
|
144
132
|
}
|
|
145
|
-
async function runBotMove(
|
|
133
|
+
async function runBotMove(services, message, session) {
|
|
146
134
|
const board = parseBoard(session.board);
|
|
147
135
|
const aiMark = session.player_o === BOT_ID ? O : X;
|
|
148
136
|
const cell = bestMove(board, aiMark);
|
|
@@ -152,9 +140,9 @@ async function runBotMove(plugin, services, message, session) {
|
|
|
152
140
|
...message,
|
|
153
141
|
$sender: { ...message.$sender, id: BOT_ID, name: '机器人' },
|
|
154
142
|
};
|
|
155
|
-
return handleMove(
|
|
143
|
+
return handleMove(services, fakeMessage, session.id, cell);
|
|
156
144
|
}
|
|
157
|
-
export async function startBotGame(
|
|
145
|
+
export async function startBotGame(services, message) {
|
|
158
146
|
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
159
147
|
const active = await services.session.getActiveByChannel(ch);
|
|
160
148
|
if (active)
|
|
@@ -168,13 +156,14 @@ export async function startBotGame(plugin, services, message) {
|
|
|
168
156
|
boardJson: JSON.stringify([0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
169
157
|
});
|
|
170
158
|
const status = `${formatRosterLine(session)} · 你先手 (✕)`;
|
|
171
|
-
const boardText =
|
|
172
|
-
|
|
173
|
-
return `${boardText}\n\n开局成功!回复数字 1–9 落子。`;
|
|
174
|
-
}
|
|
175
|
-
return '开局成功!点击棋盘或回复数字落子。';
|
|
159
|
+
const boardText = sendOrEditBoard(message, session, status, false);
|
|
160
|
+
return `${boardText}\n\n开局成功!回复数字 1–9 落子。`;
|
|
176
161
|
}
|
|
177
|
-
export async function startPvpGame(
|
|
162
|
+
export async function startPvpGame(services, message, playerX, playerO) {
|
|
163
|
+
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
164
|
+
const active = await services.session.getActiveByChannel(ch);
|
|
165
|
+
if (active)
|
|
166
|
+
return '当前频道已有进行中的对局。';
|
|
178
167
|
const session = await services.session.createSession({
|
|
179
168
|
message,
|
|
180
169
|
playerX: playerX.id,
|
|
@@ -185,8 +174,6 @@ export async function startPvpGame(plugin, services, message, playerX, playerO)
|
|
|
185
174
|
});
|
|
186
175
|
const opener = formatPlayerWithMark(session.player_x, session.player_x_name, '✕');
|
|
187
176
|
const status = `${formatRosterLine(session)} · 先手:${opener}`;
|
|
188
|
-
|
|
189
|
-
if (!plugin)
|
|
190
|
-
return boardText;
|
|
177
|
+
return sendOrEditBoard(message, session, status, false);
|
|
191
178
|
}
|
|
192
179
|
//# sourceMappingURL=game-flow.js.map
|
package/lib/game-flow.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"game-flow.js","sourceRoot":"","sources":["../src/game-flow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"game-flow.js","sourceRoot":"","sources":["../src/game-flow.ts"],"names":[],"mappings":"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"}
|
package/lib/session-service.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Database,
|
|
1
|
+
import type { Database, Models } from '@zhin.js/core';
|
|
2
|
+
import { type GameMessageLike } from '@zhin.js/game-kit';
|
|
2
3
|
import type { TttSessionRow } from './models.js';
|
|
3
4
|
/** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
|
|
4
5
|
export type TttDatabase = Database<unknown, Models, string>;
|
|
@@ -6,9 +7,13 @@ export type TttPlayerRef = {
|
|
|
6
7
|
id: string;
|
|
7
8
|
displayName: string;
|
|
8
9
|
};
|
|
10
|
+
/** 排队行过期时间(超时未匹配的排队视为失效) */
|
|
11
|
+
export declare const QUEUE_TTL_MS: number;
|
|
9
12
|
export declare class QueueService {
|
|
10
13
|
private readonly db;
|
|
11
14
|
constructor(db: TttDatabase);
|
|
15
|
+
/** 清掉频道内过期排队行(ttt_queue 无 TTL,靠 join 时顺带清理) */
|
|
16
|
+
private pruneExpired;
|
|
12
17
|
join(channel: string, userId: string, displayName?: string): Promise<{
|
|
13
18
|
queued: boolean;
|
|
14
19
|
position: number;
|
|
@@ -28,7 +33,7 @@ export declare class SessionService {
|
|
|
28
33
|
/** 根据棋盘消息 ID 查找进行中的局(QQ 等 action 带 sourceMessageId) */
|
|
29
34
|
getActiveByBoardMessageId(messageId: string): Promise<TttSessionRow | null>;
|
|
30
35
|
createSession(input: {
|
|
31
|
-
message:
|
|
36
|
+
message: GameMessageLike;
|
|
32
37
|
playerX: string;
|
|
33
38
|
playerO: string;
|
|
34
39
|
playerXName?: string;
|
package/lib/session-service.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { channelKey, boardMessageMatches, generateCompactId } from '@zhin.js/game-kit';
|
|
2
|
+
/** 排队行过期时间(超时未匹配的排队视为失效) */
|
|
3
|
+
export const QUEUE_TTL_MS = 10 * 60 * 1000;
|
|
2
4
|
function getModel(db, name) {
|
|
3
5
|
const model = db.models.get(name);
|
|
4
6
|
if (!model) {
|
|
@@ -11,8 +13,20 @@ export class QueueService {
|
|
|
11
13
|
constructor(db) {
|
|
12
14
|
this.db = db;
|
|
13
15
|
}
|
|
16
|
+
/** 清掉频道内过期排队行(ttt_queue 无 TTL,靠 join 时顺带清理) */
|
|
17
|
+
async pruneExpired(channel) {
|
|
18
|
+
const q = getModel(this.db, 'ttt_queue');
|
|
19
|
+
const cutoff = Date.now() - QUEUE_TTL_MS;
|
|
20
|
+
const rows = await q.findAll({ channel_key: channel });
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
if (row.joined_at < cutoff) {
|
|
23
|
+
await q.deleteWhere({ channel_key: channel, user_id: row.user_id });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
14
27
|
async join(channel, userId, displayName) {
|
|
15
28
|
const q = getModel(this.db, 'ttt_queue');
|
|
29
|
+
await this.pruneExpired(channel);
|
|
16
30
|
const existing = await q.findAll({ channel_key: channel, user_id: userId });
|
|
17
31
|
const all = await q.findAll({ channel_key: channel });
|
|
18
32
|
all.sort((a, b) => a.joined_at - b.joined_at);
|
|
@@ -43,7 +57,9 @@ export class QueueService {
|
|
|
43
57
|
rows.sort((a, b) => a.joined_at - b.joined_at);
|
|
44
58
|
if (rows.length < 2)
|
|
45
59
|
return null;
|
|
46
|
-
|
|
60
|
+
// 只删除匹配到的两人,保留队列中其余的等待者
|
|
61
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[0].user_id });
|
|
62
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[1].user_id });
|
|
47
63
|
const toRef = (userId, userName) => ({
|
|
48
64
|
id: userId,
|
|
49
65
|
displayName: userName.trim() || userId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-service.js","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"session-service.js","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"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"}
|
package/lib/ttt-command.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type GameMessageLike } from '@zhin.js/game-kit';
|
|
2
2
|
import type { SessionServices } from './session-service.js';
|
|
3
3
|
export declare const TTT_HELP: string;
|
|
4
|
-
export declare function runTttCommand(
|
|
5
|
-
/** Plugin Runtime / smoke: text-only, no Adapter.editMessage. */
|
|
6
|
-
export declare function runTttCommandText(services: SessionServices, message: Message<any>, action: string): Promise<string>;
|
|
4
|
+
export declare function runTttCommand(services: SessionServices, message: GameMessageLike, action: string): Promise<string>;
|
package/lib/ttt-command.js
CHANGED
|
@@ -10,7 +10,7 @@ export const TTT_HELP = [
|
|
|
10
10
|
'井字棋 认输 — 结束当前局',
|
|
11
11
|
'井字棋 观战 — 订阅观战推送',
|
|
12
12
|
].join('\n');
|
|
13
|
-
export async function runTttCommand(
|
|
13
|
+
export async function runTttCommand(services, message, action) {
|
|
14
14
|
const ch = channelKey(message);
|
|
15
15
|
const userId = message.$sender.id;
|
|
16
16
|
if (!action || action === 'help') {
|
|
@@ -36,7 +36,7 @@ export async function runTttCommand(plugin, services, message, action) {
|
|
|
36
36
|
const pair = await services.queue.tryMatch(ch);
|
|
37
37
|
if (pair) {
|
|
38
38
|
const [px, po] = pair;
|
|
39
|
-
const board = await startPvpGame(
|
|
39
|
+
const board = await startPvpGame(services, message, px, po);
|
|
40
40
|
const matchLine = `匹配成功!${formatPlayerWithMark(px.id, px.displayName, '✕')} vs ${formatPlayerWithMark(po.id, po.displayName, '○')}`;
|
|
41
41
|
return board ? `${matchLine}\n\n${board}` : matchLine;
|
|
42
42
|
}
|
|
@@ -47,7 +47,7 @@ export async function runTttCommand(plugin, services, message, action) {
|
|
|
47
47
|
return ok ? '已离开排队。' : '你不在排队中。';
|
|
48
48
|
}
|
|
49
49
|
if (action === 'bot') {
|
|
50
|
-
return startBotGame(
|
|
50
|
+
return startBotGame(services, message);
|
|
51
51
|
}
|
|
52
52
|
if (action === 'quit') {
|
|
53
53
|
const row = await services.session.getActiveForUser(ch, userId);
|
|
@@ -65,8 +65,4 @@ export async function runTttCommand(plugin, services, message, action) {
|
|
|
65
65
|
}
|
|
66
66
|
return `未知子命令:${action}\n\n${TTT_HELP}`;
|
|
67
67
|
}
|
|
68
|
-
/** Plugin Runtime / smoke: text-only, no Adapter.editMessage. */
|
|
69
|
-
export async function runTttCommandText(services, message, action) {
|
|
70
|
-
return (await runTttCommand(null, services, message, action)) ?? '';
|
|
71
|
-
}
|
|
72
68
|
//# sourceMappingURL=ttt-command.js.map
|
package/lib/ttt-command.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ttt-command.js","sourceRoot":"","sources":["../src/ttt-command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ttt-command.js","sourceRoot":"","sources":["../src/ttt-command.ts"],"names":[],"mappings":"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"}
|
package/middlewares/ttt-alias.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineGameCommandAliasMiddleware,
|
|
3
|
+
isTttAction,
|
|
3
4
|
messageFromCommandInput,
|
|
4
5
|
normalizeTttAction,
|
|
5
6
|
} from '@zhin.js/game-kit';
|
|
6
|
-
import { TTT_HELP,
|
|
7
|
+
import { TTT_HELP, runTttCommand } from '../src/ttt-command.js';
|
|
7
8
|
import { resolveGameServices } from '../src/runtime-store.js';
|
|
8
9
|
|
|
9
10
|
export default defineGameCommandAliasMiddleware({
|
|
10
11
|
aliases: ['井字棋', 'ttt'],
|
|
11
12
|
async run(action, input, context) {
|
|
12
13
|
const normalized = normalizeTttAction(String(action ?? ''));
|
|
13
|
-
if (
|
|
14
|
+
if (normalized === 'help') return TTT_HELP;
|
|
15
|
+
// action 无法识别:放行给后续中间件,避免劫持普通聊天
|
|
16
|
+
if (!isTttAction(normalized)) return null;
|
|
14
17
|
const services = resolveGameServices(context);
|
|
15
18
|
const message = messageFromCommandInput(input);
|
|
16
|
-
return
|
|
19
|
+
return runTttCommand(services, message, normalized);
|
|
17
20
|
},
|
|
18
21
|
});
|
|
@@ -34,7 +34,7 @@ export default defineMiddleware<Message>({
|
|
|
34
34
|
if (restart?.choiceId === 'restart') {
|
|
35
35
|
const session = await services.session.getById(restart.sessionId);
|
|
36
36
|
if (session?.channel_key === ch) {
|
|
37
|
-
const reply = await restartFromTerminal(
|
|
37
|
+
const reply = await restartFromTerminal(services, message, restart.sessionId);
|
|
38
38
|
if (reply) await context.input.$reply(reply);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
@@ -43,7 +43,7 @@ export default defineMiddleware<Message>({
|
|
|
43
43
|
if (move) {
|
|
44
44
|
const session = await services.session.getById(move.sessionId);
|
|
45
45
|
if (session?.channel_key === ch) {
|
|
46
|
-
const reply = await handleMove(
|
|
46
|
+
const reply = await handleMove(services, message, move.sessionId, move.cell);
|
|
47
47
|
if (reply) await context.input.$reply(reply);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
@@ -65,7 +65,7 @@ export default defineMiddleware<Message>({
|
|
|
65
65
|
await next();
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
const reply = await handleMove(
|
|
68
|
+
const reply = await handleMove(services, message, session.id, move.cell);
|
|
69
69
|
if (reply) await context.input.$reply(reply);
|
|
70
70
|
},
|
|
71
71
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-tic-tac-toe",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Zhin.js 跨平台井字棋 (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zhin.js/command": "1.0.
|
|
28
|
-
"@zhin.js/core": "1.
|
|
29
|
-
"@zhin.js/game-kit": "
|
|
30
|
-
"@zhin.js/middleware": "1.0.
|
|
31
|
-
"@zhin.js/plugin-runtime": "1.
|
|
27
|
+
"@zhin.js/command": "1.0.3",
|
|
28
|
+
"@zhin.js/core": "1.4.1",
|
|
29
|
+
"@zhin.js/game-kit": "2.0.1",
|
|
30
|
+
"@zhin.js/middleware": "1.0.3",
|
|
31
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^6.0.3",
|
package/src/board-view.ts
CHANGED
package/src/game-flow.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { plainTextFromSendContent, recordGameOutcome } from '@zhin.js/game-kit';
|
|
1
|
+
import { plainTextFromSendContent, recordGameOutcome, type GameMessageLike } from '@zhin.js/game-kit';
|
|
3
2
|
import type { TttSessionRow } from './models.js';
|
|
4
3
|
import { buildBoardInteractive } from './board-view.js';
|
|
5
4
|
import {
|
|
@@ -32,15 +31,18 @@ export function isBotSession(session: TttSessionRow): boolean {
|
|
|
32
31
|
return session.player_o === BOT_ID || session.player_x === BOT_ID;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Plugin Runtime: render the board as text. Interactive in-place board editing
|
|
36
|
+
* (the old Adapter.editMessage path) is not part of the runtime flow; commands
|
|
37
|
+
* and the choice middleware return fresh text each turn.
|
|
38
|
+
*/
|
|
39
|
+
export function sendOrEditBoard(
|
|
40
|
+
message: GameMessageLike,
|
|
39
41
|
session: TttSessionRow,
|
|
40
42
|
statusLine: string,
|
|
41
43
|
terminal = false,
|
|
42
44
|
highlight?: number[],
|
|
43
|
-
):
|
|
45
|
+
): string {
|
|
44
46
|
const board = parseBoard(session.board);
|
|
45
47
|
const content = buildBoardInteractive({
|
|
46
48
|
sessionId: session.id,
|
|
@@ -53,50 +55,37 @@ export async function sendOrEditBoard(
|
|
|
53
55
|
channelType: message.$channel.type,
|
|
54
56
|
});
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const adapter = plugin.root.inject(message.$adapter) as Adapter;
|
|
59
|
-
|
|
60
|
-
if (session.board_message_id) {
|
|
61
|
-
const msgId = await adapter.editMessage({
|
|
62
|
-
messageId: session.board_message_id,
|
|
63
|
-
context: String(message.$adapter),
|
|
64
|
-
endpoint: message.$endpoint,
|
|
65
|
-
id: message.$channel.id,
|
|
66
|
-
type: message.$channel.type,
|
|
67
|
-
content,
|
|
68
|
-
});
|
|
69
|
-
if (msgId !== session.board_message_id) {
|
|
70
|
-
await services.session.updateSession(session.id, { board_message_id: msgId });
|
|
71
|
-
}
|
|
72
|
-
return msgId;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const msgId = await message.$reply?.(content);
|
|
76
|
-
if (msgId) {
|
|
77
|
-
await services.session.updateSession(session.id, { board_message_id: msgId });
|
|
78
|
-
}
|
|
79
|
-
return msgId ?? '';
|
|
58
|
+
return plainTextFromSendContent(content);
|
|
80
59
|
}
|
|
81
60
|
|
|
82
61
|
export async function restartFromTerminal(
|
|
83
|
-
plugin: Plugin | null,
|
|
84
62
|
services: SessionServices,
|
|
85
|
-
message:
|
|
63
|
+
message: GameMessageLike,
|
|
86
64
|
sessionId: string,
|
|
87
65
|
): Promise<string | null> {
|
|
88
66
|
const session = await services.session.getById(sessionId);
|
|
89
67
|
if (!session) return '对局不存在。';
|
|
68
|
+
if (session.status === 'active') return '对局尚未结束,无法重开。';
|
|
90
69
|
const mark = playerMark(message.$sender.id, {
|
|
91
70
|
playerX: session.player_x,
|
|
92
71
|
playerO: session.player_o,
|
|
93
72
|
});
|
|
94
73
|
if (!mark) return '你不是本局玩家。';
|
|
95
74
|
await services.session.updateSession(session.id, { status: 'aborted', winner: 0 });
|
|
96
|
-
|
|
75
|
+
// PvP 局重开仍是 PvP(原班人马),不静默降级为人机
|
|
76
|
+
if (!isBotSession(session)) {
|
|
77
|
+
const board = await startPvpGame(
|
|
78
|
+
services,
|
|
79
|
+
message,
|
|
80
|
+
{ id: session.player_x, displayName: session.player_x_name },
|
|
81
|
+
{ id: session.player_o, displayName: session.player_o_name },
|
|
82
|
+
);
|
|
83
|
+
if (board.includes('已有')) return board;
|
|
84
|
+
return board;
|
|
85
|
+
}
|
|
86
|
+
const hint = await startBotGame(services, message);
|
|
97
87
|
if (hint.includes('已有')) return hint;
|
|
98
|
-
|
|
99
|
-
return plugin ? null : hint;
|
|
88
|
+
return hint;
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
function turnCell(session: TttSessionRow): Cell {
|
|
@@ -108,9 +97,8 @@ function playerIdForTurn(session: TttSessionRow): string {
|
|
|
108
97
|
}
|
|
109
98
|
|
|
110
99
|
export async function handleMove(
|
|
111
|
-
plugin: Plugin | null,
|
|
112
100
|
services: SessionServices,
|
|
113
|
-
message:
|
|
101
|
+
message: GameMessageLike,
|
|
114
102
|
sessionId: string,
|
|
115
103
|
cell: number,
|
|
116
104
|
): Promise<string | null> {
|
|
@@ -166,7 +154,7 @@ export async function handleMove(
|
|
|
166
154
|
const humanMsg = {
|
|
167
155
|
...message,
|
|
168
156
|
$sender: { ...message.$sender, id: humanId, name: humanName },
|
|
169
|
-
} as
|
|
157
|
+
} as GameMessageLike;
|
|
170
158
|
if (win) {
|
|
171
159
|
const humanMark = playerMark(humanId, {
|
|
172
160
|
playerX: updated.player_x,
|
|
@@ -181,29 +169,24 @@ export async function handleMove(
|
|
|
181
169
|
|
|
182
170
|
if (win) {
|
|
183
171
|
const status = formatWinHeadline(updated, win.winner);
|
|
184
|
-
|
|
185
|
-
// text-only 模式(plugin===null)下 sendOrEditBoard 的唯一输出就是返回文本
|
|
186
|
-
return plugin ? null : text;
|
|
172
|
+
return sendOrEditBoard(message, updated, status, true, win.line);
|
|
187
173
|
}
|
|
188
174
|
if (draw) {
|
|
189
|
-
|
|
190
|
-
return plugin ? null : text;
|
|
175
|
+
return sendOrEditBoard(message, updated, '平局。', true);
|
|
191
176
|
}
|
|
192
177
|
|
|
193
178
|
// 人机:玩家落子后立刻由服务端代下,避免先发「轮到机器人」再发终盘(QQ 被动消息多耗一次)
|
|
194
179
|
if (isBotSession(updated) && playerIdForTurn(updated) === BOT_ID) {
|
|
195
|
-
return runBotMove(
|
|
180
|
+
return runBotMove(services, message, updated);
|
|
196
181
|
}
|
|
197
182
|
|
|
198
183
|
const status = formatTurnStatus(updated, moveCount);
|
|
199
|
-
|
|
200
|
-
return plugin ? null : text;
|
|
184
|
+
return sendOrEditBoard(message, updated, status, false);
|
|
201
185
|
}
|
|
202
186
|
|
|
203
187
|
async function runBotMove(
|
|
204
|
-
plugin: Plugin | null,
|
|
205
188
|
services: SessionServices,
|
|
206
|
-
message:
|
|
189
|
+
message: GameMessageLike,
|
|
207
190
|
session: TttSessionRow,
|
|
208
191
|
): Promise<string | null> {
|
|
209
192
|
const board = parseBoard(session.board);
|
|
@@ -214,15 +197,14 @@ async function runBotMove(
|
|
|
214
197
|
const fakeMessage = {
|
|
215
198
|
...message,
|
|
216
199
|
$sender: { ...message.$sender, id: BOT_ID, name: '机器人' },
|
|
217
|
-
} as
|
|
200
|
+
} as GameMessageLike;
|
|
218
201
|
|
|
219
|
-
return handleMove(
|
|
202
|
+
return handleMove(services, fakeMessage, session.id, cell);
|
|
220
203
|
}
|
|
221
204
|
|
|
222
205
|
export async function startBotGame(
|
|
223
|
-
plugin: Plugin | null,
|
|
224
206
|
services: SessionServices,
|
|
225
|
-
message:
|
|
207
|
+
message: GameMessageLike,
|
|
226
208
|
): Promise<string> {
|
|
227
209
|
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
228
210
|
const active = await services.session.getActiveByChannel(ch);
|
|
@@ -238,20 +220,20 @@ export async function startBotGame(
|
|
|
238
220
|
});
|
|
239
221
|
|
|
240
222
|
const status = `${formatRosterLine(session)} · 你先手 (✕)`;
|
|
241
|
-
const boardText =
|
|
242
|
-
|
|
243
|
-
return `${boardText}\n\n开局成功!回复数字 1–9 落子。`;
|
|
244
|
-
}
|
|
245
|
-
return '开局成功!点击棋盘或回复数字落子。';
|
|
223
|
+
const boardText = sendOrEditBoard(message, session, status, false);
|
|
224
|
+
return `${boardText}\n\n开局成功!回复数字 1–9 落子。`;
|
|
246
225
|
}
|
|
247
226
|
|
|
248
227
|
export async function startPvpGame(
|
|
249
|
-
plugin: Plugin | null,
|
|
250
228
|
services: SessionServices,
|
|
251
|
-
message:
|
|
229
|
+
message: GameMessageLike,
|
|
252
230
|
playerX: TttPlayerRef,
|
|
253
231
|
playerO: TttPlayerRef,
|
|
254
|
-
): Promise<string
|
|
232
|
+
): Promise<string> {
|
|
233
|
+
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
234
|
+
const active = await services.session.getActiveByChannel(ch);
|
|
235
|
+
if (active) return '当前频道已有进行中的对局。';
|
|
236
|
+
|
|
255
237
|
const session = await services.session.createSession({
|
|
256
238
|
message,
|
|
257
239
|
playerX: playerX.id,
|
|
@@ -262,6 +244,5 @@ export async function startPvpGame(
|
|
|
262
244
|
});
|
|
263
245
|
const opener = formatPlayerWithMark(session.player_x, session.player_x_name, '✕');
|
|
264
246
|
const status = `${formatRosterLine(session)} · 先手:${opener}`;
|
|
265
|
-
|
|
266
|
-
if (!plugin) return boardText;
|
|
247
|
+
return sendOrEditBoard(message, session, status, false);
|
|
267
248
|
}
|
package/src/session-service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Database,
|
|
2
|
-
import { channelKey, boardMessageMatches, generateCompactId } from '@zhin.js/game-kit';
|
|
1
|
+
import type { Database, Models, RelatedModel } from '@zhin.js/core';
|
|
2
|
+
import { channelKey, boardMessageMatches, generateCompactId , type GameMessageLike } from '@zhin.js/game-kit';
|
|
3
3
|
import type { TttModelName, TttSessionRow } from './models.js';
|
|
4
4
|
|
|
5
5
|
/** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
|
|
@@ -9,6 +9,9 @@ type TttModel<K extends TttModelName> = RelatedModel<unknown, Models, K>;
|
|
|
9
9
|
|
|
10
10
|
export type TttPlayerRef = { id: string; displayName: string };
|
|
11
11
|
|
|
12
|
+
/** 排队行过期时间(超时未匹配的排队视为失效) */
|
|
13
|
+
export const QUEUE_TTL_MS = 10 * 60 * 1000;
|
|
14
|
+
|
|
12
15
|
function getModel<K extends TttModelName>(db: TttDatabase, name: K): TttModel<K> {
|
|
13
16
|
const model = db.models.get(name);
|
|
14
17
|
if (!model) {
|
|
@@ -20,12 +23,25 @@ function getModel<K extends TttModelName>(db: TttDatabase, name: K): TttModel<K>
|
|
|
20
23
|
export class QueueService {
|
|
21
24
|
constructor(private readonly db: TttDatabase) {}
|
|
22
25
|
|
|
26
|
+
/** 清掉频道内过期排队行(ttt_queue 无 TTL,靠 join 时顺带清理) */
|
|
27
|
+
private async pruneExpired(channel: string): Promise<void> {
|
|
28
|
+
const q = getModel(this.db, 'ttt_queue');
|
|
29
|
+
const cutoff = Date.now() - QUEUE_TTL_MS;
|
|
30
|
+
const rows = await q.findAll({ channel_key: channel });
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
if (row.joined_at < cutoff) {
|
|
33
|
+
await q.deleteWhere({ channel_key: channel, user_id: row.user_id });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
async join(
|
|
24
39
|
channel: string,
|
|
25
40
|
userId: string,
|
|
26
41
|
displayName?: string,
|
|
27
42
|
): Promise<{ queued: boolean; position: number }> {
|
|
28
43
|
const q = getModel(this.db, 'ttt_queue');
|
|
44
|
+
await this.pruneExpired(channel);
|
|
29
45
|
const existing = await q.findAll({ channel_key: channel, user_id: userId });
|
|
30
46
|
const all = await q.findAll({ channel_key: channel });
|
|
31
47
|
all.sort((a, b) => a.joined_at - b.joined_at);
|
|
@@ -57,7 +73,9 @@ export class QueueService {
|
|
|
57
73
|
const rows = await q.findAll({ channel_key: channel });
|
|
58
74
|
rows.sort((a, b) => a.joined_at - b.joined_at);
|
|
59
75
|
if (rows.length < 2) return null;
|
|
60
|
-
|
|
76
|
+
// 只删除匹配到的两人,保留队列中其余的等待者
|
|
77
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[0]!.user_id });
|
|
78
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[1]!.user_id });
|
|
61
79
|
const toRef = (userId: string, userName: string) => ({
|
|
62
80
|
id: userId,
|
|
63
81
|
displayName: userName.trim() || userId,
|
|
@@ -111,7 +129,7 @@ export class SessionService {
|
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
async createSession(input: {
|
|
114
|
-
message:
|
|
132
|
+
message: GameMessageLike;
|
|
115
133
|
playerX: string;
|
|
116
134
|
playerO: string;
|
|
117
135
|
playerXName?: string;
|
package/src/ttt-command.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { channelKey } from '@zhin.js/game-kit';
|
|
1
|
+
import { channelKey, type GameMessageLike } from '@zhin.js/game-kit';
|
|
3
2
|
import { formatPlayerWithMark, formatRosterLine } from './player-label.js';
|
|
4
3
|
import { startBotGame, startPvpGame } from './game-flow.js';
|
|
5
4
|
import type { SessionServices } from './session-service.js';
|
|
@@ -15,11 +14,10 @@ export const TTT_HELP = [
|
|
|
15
14
|
].join('\n');
|
|
16
15
|
|
|
17
16
|
export async function runTttCommand(
|
|
18
|
-
plugin: Plugin | null,
|
|
19
17
|
services: SessionServices,
|
|
20
|
-
message:
|
|
18
|
+
message: GameMessageLike,
|
|
21
19
|
action: string,
|
|
22
|
-
): Promise<string
|
|
20
|
+
): Promise<string> {
|
|
23
21
|
const ch = channelKey(message);
|
|
24
22
|
const userId = message.$sender.id;
|
|
25
23
|
|
|
@@ -43,7 +41,7 @@ export async function runTttCommand(
|
|
|
43
41
|
const pair = await services.queue.tryMatch(ch);
|
|
44
42
|
if (pair) {
|
|
45
43
|
const [px, po] = pair;
|
|
46
|
-
const board = await startPvpGame(
|
|
44
|
+
const board = await startPvpGame(services, message, px, po);
|
|
47
45
|
const matchLine = `匹配成功!${formatPlayerWithMark(px.id, px.displayName, '✕')} vs ${formatPlayerWithMark(po.id, po.displayName, '○')}`;
|
|
48
46
|
return board ? `${matchLine}\n\n${board}` : matchLine;
|
|
49
47
|
}
|
|
@@ -56,7 +54,7 @@ export async function runTttCommand(
|
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
if (action === 'bot') {
|
|
59
|
-
return startBotGame(
|
|
57
|
+
return startBotGame(services, message);
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
if (action === 'quit') {
|
|
@@ -75,12 +73,3 @@ export async function runTttCommand(
|
|
|
75
73
|
|
|
76
74
|
return `未知子命令:${action}\n\n${TTT_HELP}`;
|
|
77
75
|
}
|
|
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
|
-
}
|