@zhin.js/plugin-tic-tac-toe 1.0.4 → 1.0.5
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=].js +16 -0
- package/lib/game-flow.d.ts +7 -11
- package/lib/game-flow.js +12 -39
- package/lib/game-flow.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/models.d.ts +0 -1
- package/lib/models.js +0 -1
- package/lib/models.js.map +1 -1
- package/lib/session-service.d.ts +2 -9
- package/lib/session-service.js +38 -50
- package/lib/session-service.js.map +1 -1
- package/lib/ttt-command.d.ts +2 -2
- package/lib/ttt-command.js +6 -2
- package/lib/ttt-command.js.map +1 -1
- package/middlewares/ttt-alias.js +18 -0
- package/middlewares/ttt-choice.js +66 -0
- package/package.json +7 -7
- package/plugin.js +84 -0
- package/src/game-flow.ts +19 -45
- package/src/index.ts +0 -1
- package/src/models.ts +0 -2
- package/src/session-service.ts +43 -54
- package/src/ttt-command.ts +11 -3
- package/lib/memory-db.d.ts +0 -6
- package/lib/memory-db.js +0 -18
- package/lib/memory-db.js.map +0 -1
- package/plugin.ts +0 -64
- package/src/memory-db.ts +0 -22
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from '@zhin.js/command';
|
|
3
|
+
import { messageFromCommandInput, normalizeTttAction } from '@zhin.js/game-kit';
|
|
4
|
+
import { TTT_HELP, runTttCommand } from "../../lib/ttt-command.js";
|
|
5
|
+
import { resolveGameServices } from "../../lib/runtime-store.js";
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
description: 'Tic Tac Toe',
|
|
8
|
+
async execute({ params, input, use, owner }) {
|
|
9
|
+
const action = normalizeTttAction(String(params.action ?? ''));
|
|
10
|
+
if (!action || action === 'help')
|
|
11
|
+
return TTT_HELP;
|
|
12
|
+
const services = resolveGameServices({ use, owner });
|
|
13
|
+
const message = messageFromCommandInput(input);
|
|
14
|
+
return runTttCommand(services, message, action);
|
|
15
|
+
},
|
|
16
|
+
});
|
package/lib/game-flow.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { GameMessageLike, GameReply } 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
|
-
|
|
12
|
-
export declare function
|
|
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>;
|
|
7
|
+
/** Render the canonical board reply; delivery belongs to the Runtime Host. */
|
|
8
|
+
export declare function renderBoard(message: GameMessageLike, session: TttSessionRow, statusLine: string, terminal?: boolean, highlight?: number[]): GameReply;
|
|
9
|
+
export declare function restartFromTerminal(services: SessionServices, message: GameMessageLike, sessionId: string): Promise<GameReply | null>;
|
|
10
|
+
export declare function handleMove(services: SessionServices, message: GameMessageLike, sessionId: string, cell: number): Promise<GameReply | null>;
|
|
11
|
+
export declare function startBotGame(services: SessionServices, message: GameMessageLike): Promise<GameReply>;
|
|
12
|
+
export declare function startPvpGame(services: SessionServices, message: GameMessageLike, playerX: TttPlayerRef, playerO: TttPlayerRef): Promise<GameReply>;
|
package/lib/game-flow.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { plainTextFromSendContent, recordGameOutcome } from '@zhin.js/game-kit';
|
|
2
1
|
import { buildBoardInteractive } from './board-view.js';
|
|
3
2
|
import { applyMove, bestMove, checkWinner, isDraw, parseBoard, playerMark, validMove, X, O, cellLabel, } from './engine.js';
|
|
4
3
|
import { BOT_ID, formatPlayerWithMark, formatRosterLine, formatTurnStatus, formatWinHeadline, playerRefForMark, senderDisplayName, } from './player-label.js';
|
|
@@ -6,12 +5,8 @@ export { BOT_ID };
|
|
|
6
5
|
export function isBotSession(session) {
|
|
7
6
|
return session.player_o === BOT_ID || session.player_x === BOT_ID;
|
|
8
7
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
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) {
|
|
8
|
+
/** Render the canonical board reply; delivery belongs to the Runtime Host. */
|
|
9
|
+
export function renderBoard(message, session, statusLine, terminal = false, highlight) {
|
|
15
10
|
const board = parseBoard(session.board);
|
|
16
11
|
const content = buildBoardInteractive({
|
|
17
12
|
sessionId: session.id,
|
|
@@ -23,7 +18,7 @@ export function sendOrEditBoard(message, session, statusLine, terminal = false,
|
|
|
23
18
|
highlight,
|
|
24
19
|
channelType: message.$channel.type,
|
|
25
20
|
});
|
|
26
|
-
return
|
|
21
|
+
return content;
|
|
27
22
|
}
|
|
28
23
|
export async function restartFromTerminal(services, message, sessionId) {
|
|
29
24
|
const session = await services.session.getById(sessionId);
|
|
@@ -41,12 +36,12 @@ export async function restartFromTerminal(services, message, sessionId) {
|
|
|
41
36
|
// PvP 局重开仍是 PvP(原班人马),不静默降级为人机
|
|
42
37
|
if (!isBotSession(session)) {
|
|
43
38
|
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('已有'))
|
|
39
|
+
if (typeof board === 'string' && board.includes('已有'))
|
|
45
40
|
return board;
|
|
46
41
|
return board;
|
|
47
42
|
}
|
|
48
43
|
const hint = await startBotGame(services, message);
|
|
49
|
-
if (hint.includes('已有'))
|
|
44
|
+
if (typeof hint === 'string' && hint.includes('已有'))
|
|
50
45
|
return hint;
|
|
51
46
|
return hint;
|
|
52
47
|
}
|
|
@@ -93,42 +88,19 @@ export async function handleMove(services, message, sessionId, cell) {
|
|
|
93
88
|
winner: win ? win.winner : 0,
|
|
94
89
|
});
|
|
95
90
|
const updated = (await services.session.getById(session.id));
|
|
96
|
-
if (win || draw) {
|
|
97
|
-
const humanId = isBotSession(updated)
|
|
98
|
-
? (updated.player_x === BOT_ID ? updated.player_o : updated.player_x)
|
|
99
|
-
: message.$sender.id;
|
|
100
|
-
const humanName = isBotSession(updated)
|
|
101
|
-
? (updated.player_x === BOT_ID ? updated.player_o_name : updated.player_x_name)
|
|
102
|
-
: String(message.$sender.name ?? message.$sender.id);
|
|
103
|
-
const humanMsg = {
|
|
104
|
-
...message,
|
|
105
|
-
$sender: { ...message.$sender, id: humanId, name: humanName },
|
|
106
|
-
};
|
|
107
|
-
if (win) {
|
|
108
|
-
const humanMark = playerMark(humanId, {
|
|
109
|
-
playerX: updated.player_x,
|
|
110
|
-
playerO: updated.player_o,
|
|
111
|
-
});
|
|
112
|
-
const humanWon = humanMark === win.winner;
|
|
113
|
-
void recordGameOutcome(humanMsg, 'ttt', humanWon ? 'won' : 'lost', humanWon ? 20 : 0);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
void recordGameOutcome(humanMsg, 'ttt', 'draw');
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
91
|
if (win) {
|
|
120
92
|
const status = formatWinHeadline(updated, win.winner);
|
|
121
|
-
return
|
|
93
|
+
return renderBoard(message, updated, status, true, win.line);
|
|
122
94
|
}
|
|
123
95
|
if (draw) {
|
|
124
|
-
return
|
|
96
|
+
return renderBoard(message, updated, '平局。', true);
|
|
125
97
|
}
|
|
126
98
|
// 人机:玩家落子后立刻由服务端代下,避免先发「轮到机器人」再发终盘(QQ 被动消息多耗一次)
|
|
127
99
|
if (isBotSession(updated) && playerIdForTurn(updated) === BOT_ID) {
|
|
128
100
|
return runBotMove(services, message, updated);
|
|
129
101
|
}
|
|
130
102
|
const status = formatTurnStatus(updated, moveCount);
|
|
131
|
-
return
|
|
103
|
+
return renderBoard(message, updated, status, false);
|
|
132
104
|
}
|
|
133
105
|
async function runBotMove(services, message, session) {
|
|
134
106
|
const board = parseBoard(session.board);
|
|
@@ -156,8 +128,9 @@ export async function startBotGame(services, message) {
|
|
|
156
128
|
boardJson: JSON.stringify([0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
157
129
|
});
|
|
158
130
|
const status = `${formatRosterLine(session)} · 你先手 (✕)`;
|
|
159
|
-
const
|
|
160
|
-
|
|
131
|
+
const board = renderBoard(message, session, status, false);
|
|
132
|
+
const success = '\n\n开局成功!点击空格或回复数字 1–9 落子。';
|
|
133
|
+
return Array.isArray(board) ? [...board, success] : [board, success];
|
|
161
134
|
}
|
|
162
135
|
export async function startPvpGame(services, message, playerX, playerO) {
|
|
163
136
|
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
@@ -174,6 +147,6 @@ export async function startPvpGame(services, message, playerX, playerO) {
|
|
|
174
147
|
});
|
|
175
148
|
const opener = formatPlayerWithMark(session.player_x, session.player_x_name, '✕');
|
|
176
149
|
const status = `${formatRosterLine(session)} · 先手:${opener}`;
|
|
177
|
-
return
|
|
150
|
+
return renderBoard(message, session, status, false);
|
|
178
151
|
}
|
|
179
152
|
//# 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/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { TTT_HELP } from './ttt-command.js';
|
|
2
2
|
export { gameServicesToken, resolveGameServices } from './runtime-store.js';
|
|
3
|
-
export { createInMemoryTttDb, mountTttMemoryServices } from './memory-db.js';
|
|
4
3
|
/**
|
|
5
4
|
* 已由 plugin.ts 接线:commands/ 命令、middlewares/ 文本与选项中间件、
|
|
6
5
|
* 游戏大厅注册(registerRuntimeGame)、过期会话 cron(scheduleHostToken)、
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { TTT_HELP } from './ttt-command.js';
|
|
2
2
|
export { gameServicesToken, resolveGameServices } from './runtime-store.js';
|
|
3
|
-
export { createInMemoryTttDb, mountTttMemoryServices } from './memory-db.js';
|
|
4
3
|
/**
|
|
5
4
|
* 已由 plugin.ts 接线:commands/ 命令、middlewares/ 文本与选项中间件、
|
|
6
5
|
* 游戏大厅注册(registerRuntimeGame)、过期会话 cron(scheduleHostToken)、
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE5E;;;;GAIG"}
|
package/lib/models.d.ts
CHANGED
package/lib/models.js
CHANGED
|
@@ -14,7 +14,6 @@ export function defineHostTables(db) {
|
|
|
14
14
|
turn: { type: 'integer', default: 1 },
|
|
15
15
|
status: { type: 'text', default: 'active' },
|
|
16
16
|
winner: { type: 'integer', default: 0 },
|
|
17
|
-
board_message_id: { type: 'text', default: '' },
|
|
18
17
|
move_count: { type: 'integer', default: 0 },
|
|
19
18
|
updated_at: { type: 'integer', default: 0 },
|
|
20
19
|
created_at: { type: 'integer', default: 0 },
|
package/lib/models.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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"}
|
package/lib/session-service.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database, Models } from '@zhin.js/core';
|
|
2
|
-
import { type GameMessageLike } from '@zhin.js/game-kit';
|
|
2
|
+
import { BaseSessionService, type GameMessageLike } from '@zhin.js/game-kit';
|
|
3
3
|
import type { TttSessionRow } from './models.js';
|
|
4
4
|
/** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
|
|
5
5
|
export type TttDatabase = Database<unknown, Models, string>;
|
|
@@ -24,14 +24,9 @@ export declare class QueueService {
|
|
|
24
24
|
count(channel: string): Promise<number>;
|
|
25
25
|
}
|
|
26
26
|
export declare function sessionId(): string;
|
|
27
|
-
export declare class SessionService {
|
|
27
|
+
export declare class SessionService extends BaseSessionService<TttSessionRow> {
|
|
28
28
|
private readonly db;
|
|
29
29
|
constructor(db: TttDatabase);
|
|
30
|
-
getActiveByChannel(channel: string): Promise<TttSessionRow | null>;
|
|
31
|
-
getById(id: string): Promise<TttSessionRow | null>;
|
|
32
|
-
getActiveForUser(channel: string, userId: string): Promise<TttSessionRow | null>;
|
|
33
|
-
/** 根据棋盘消息 ID 查找进行中的局(QQ 等 action 带 sourceMessageId) */
|
|
34
|
-
getActiveByBoardMessageId(messageId: string): Promise<TttSessionRow | null>;
|
|
35
30
|
createSession(input: {
|
|
36
31
|
message: GameMessageLike;
|
|
37
32
|
playerX: string;
|
|
@@ -40,11 +35,9 @@ export declare class SessionService {
|
|
|
40
35
|
playerOName?: string;
|
|
41
36
|
boardJson: string;
|
|
42
37
|
}): Promise<TttSessionRow>;
|
|
43
|
-
updateSession(id: string, patch: Partial<TttSessionRow>): Promise<void>;
|
|
44
38
|
recordMove(sessionId: string, playerId: string, cell: number, moveIndex: number): Promise<void>;
|
|
45
39
|
addSpectator(sessionId: string, userId: string): Promise<void>;
|
|
46
40
|
listSpectators(sessionId: string): Promise<string[]>;
|
|
47
|
-
abortStale(idleMs: number): Promise<number>;
|
|
48
41
|
}
|
|
49
42
|
export type SessionServices = {
|
|
50
43
|
queue: QueueService;
|
package/lib/session-service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseSessionService, channelKey, generateCompactId, } from '@zhin.js/game-kit';
|
|
2
|
+
import { BOT_ID } from './player-label.js';
|
|
2
3
|
/** 排队行过期时间(超时未匹配的排队视为失效) */
|
|
3
4
|
export const QUEUE_TTL_MS = 10 * 60 * 1000;
|
|
4
5
|
function getModel(db, name) {
|
|
@@ -77,39 +78,44 @@ export class QueueService {
|
|
|
77
78
|
export function sessionId() {
|
|
78
79
|
return generateCompactId('s');
|
|
79
80
|
}
|
|
80
|
-
export class SessionService {
|
|
81
|
+
export class SessionService extends BaseSessionService {
|
|
81
82
|
db;
|
|
82
83
|
constructor(db) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
super(db, {
|
|
85
|
+
gameId: 'ttt',
|
|
86
|
+
table: 'ttt_sessions',
|
|
87
|
+
userFields: ['player_x', 'player_o'],
|
|
88
|
+
projectOutcomes: (session) => {
|
|
89
|
+
if (session.status !== 'won' && session.status !== 'draw')
|
|
90
|
+
return [];
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
id: session.player_x,
|
|
94
|
+
name: session.player_x_name,
|
|
95
|
+
mark: 1,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: session.player_o,
|
|
99
|
+
name: session.player_o_name,
|
|
100
|
+
mark: 2,
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
.filter((player) => player.id !== BOT_ID)
|
|
104
|
+
.map((player) => ({
|
|
105
|
+
userId: player.id,
|
|
106
|
+
userName: player.name,
|
|
107
|
+
result: session.status === 'draw'
|
|
108
|
+
? 'draw'
|
|
109
|
+
: session.winner === player.mark
|
|
110
|
+
? 'won'
|
|
111
|
+
: 'lost',
|
|
112
|
+
score: session.status === 'won' && session.winner === player.mark
|
|
113
|
+
? 20
|
|
114
|
+
: undefined,
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
89
117
|
});
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
async getById(id) {
|
|
93
|
-
return getModel(this.db, 'ttt_sessions').findOne({ id });
|
|
94
|
-
}
|
|
95
|
-
async getActiveForUser(channel, userId) {
|
|
96
|
-
const row = await this.getActiveByChannel(channel);
|
|
97
|
-
if (!row)
|
|
98
|
-
return null;
|
|
99
|
-
if (row.player_x === userId || row.player_o === userId)
|
|
100
|
-
return row;
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
/** 根据棋盘消息 ID 查找进行中的局(QQ 等 action 带 sourceMessageId) */
|
|
104
|
-
async getActiveByBoardMessageId(messageId) {
|
|
105
|
-
if (!messageId)
|
|
106
|
-
return null;
|
|
107
|
-
const rows = await getModel(this.db, 'ttt_sessions').findAll({});
|
|
108
|
-
for (const row of rows) {
|
|
109
|
-
if (boardMessageMatches(row.board_message_id ?? '', messageId))
|
|
110
|
-
return row;
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
118
|
+
this.db = db;
|
|
113
119
|
}
|
|
114
120
|
async createSession(input) {
|
|
115
121
|
const now = Date.now();
|
|
@@ -130,16 +136,11 @@ export class SessionService {
|
|
|
130
136
|
turn: 1,
|
|
131
137
|
status: 'active',
|
|
132
138
|
winner: 0,
|
|
133
|
-
board_message_id: '',
|
|
134
139
|
move_count: 0,
|
|
135
140
|
updated_at: now,
|
|
136
141
|
created_at: now,
|
|
137
142
|
};
|
|
138
|
-
|
|
139
|
-
return row;
|
|
140
|
-
}
|
|
141
|
-
async updateSession(id, patch) {
|
|
142
|
-
await getModel(this.db, 'ttt_sessions').updateWhere({ id }, { ...patch, updated_at: Date.now() });
|
|
143
|
+
return this.createRow(row);
|
|
143
144
|
}
|
|
144
145
|
async recordMove(sessionId, playerId, cell, moveIndex) {
|
|
145
146
|
await getModel(this.db, 'ttt_moves').create({
|
|
@@ -161,19 +162,6 @@ export class SessionService {
|
|
|
161
162
|
const rows = await getModel(this.db, 'ttt_spectators').findAll({ session_id: sessionId });
|
|
162
163
|
return rows.map((r) => r.user_id);
|
|
163
164
|
}
|
|
164
|
-
async abortStale(idleMs) {
|
|
165
|
-
const cutoff = Date.now() - idleMs;
|
|
166
|
-
const sessions = getModel(this.db, 'ttt_sessions');
|
|
167
|
-
const rows = await sessions.findAll({ status: 'active' });
|
|
168
|
-
let n = 0;
|
|
169
|
-
for (const row of rows) {
|
|
170
|
-
if (row.updated_at < cutoff) {
|
|
171
|
-
await sessions.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
|
|
172
|
-
n++;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return n;
|
|
176
|
-
}
|
|
177
165
|
}
|
|
178
166
|
export function createServices(db) {
|
|
179
167
|
return { queue: new QueueService(db), session: new SessionService(db) };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-service.js","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { type GameMessageLike } from '@zhin.js/game-kit';
|
|
1
|
+
import { type GameMessageLike, type GameReply } 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(services: SessionServices, message: GameMessageLike, action: string): Promise<
|
|
4
|
+
export declare function runTttCommand(services: SessionServices, message: GameMessageLike, action: string): Promise<GameReply>;
|
package/lib/ttt-command.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { channelKey } from '@zhin.js/game-kit';
|
|
1
|
+
import { channelKey, } from '@zhin.js/game-kit';
|
|
2
2
|
import { formatPlayerWithMark, formatRosterLine } from './player-label.js';
|
|
3
3
|
import { startBotGame, startPvpGame } from './game-flow.js';
|
|
4
4
|
export const TTT_HELP = [
|
|
@@ -38,7 +38,11 @@ export async function runTttCommand(services, message, action) {
|
|
|
38
38
|
const [px, po] = pair;
|
|
39
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
|
-
return board
|
|
41
|
+
return board
|
|
42
|
+
? Array.isArray(board)
|
|
43
|
+
? [matchLine, '\n\n', ...board]
|
|
44
|
+
: [matchLine, '\n\n', board]
|
|
45
|
+
: matchLine;
|
|
42
46
|
}
|
|
43
47
|
return `已加入排队(第 ${position} 位),凑满 2 人自动开局。`;
|
|
44
48
|
}
|
package/lib/ttt-command.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ttt-command.js","sourceRoot":"","sources":["../src/ttt-command.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ttt-command.js","sourceRoot":"","sources":["../src/ttt-command.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineGameCommandAliasMiddleware, isTttAction, messageFromCommandInput, normalizeTttAction, } from '@zhin.js/game-kit';
|
|
3
|
+
import { TTT_HELP, runTttCommand } from "../lib/ttt-command.js";
|
|
4
|
+
import { resolveGameServices } from "../lib/runtime-store.js";
|
|
5
|
+
export default defineGameCommandAliasMiddleware({
|
|
6
|
+
aliases: ['井字棋', 'ttt'],
|
|
7
|
+
async run(action, input, context) {
|
|
8
|
+
const normalized = normalizeTttAction(String(action ?? ''));
|
|
9
|
+
if (normalized === 'help')
|
|
10
|
+
return TTT_HELP;
|
|
11
|
+
// action 无法识别:放行给后续中间件,避免劫持普通聊天
|
|
12
|
+
if (!isTttAction(normalized))
|
|
13
|
+
return null;
|
|
14
|
+
const services = resolveGameServices(context);
|
|
15
|
+
const message = messageFromCommandInput(input);
|
|
16
|
+
return runTttCommand(services, message, normalized);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineMiddleware } from '@zhin.js/middleware';
|
|
3
|
+
import { channelKey, messageFromCommandInput, parseChoicePayload, resolveGameTextPayload, } from '@zhin.js/game-kit';
|
|
4
|
+
import { resolveGameServices } from "../lib/runtime-store.js";
|
|
5
|
+
import { buildFallbackMap, TTT_PREFIX, parseTttPayload } from "../lib/board-view.js";
|
|
6
|
+
import { parseBoard } from "../lib/engine.js";
|
|
7
|
+
import { handleMove, restartFromTerminal } from "../lib/game-flow.js";
|
|
8
|
+
/**
|
|
9
|
+
* 文本入口:按钮 payload(`ttt:session:0-8` 或 `ttt:session:restart`)
|
|
10
|
+
* 与裸数字 fallback(按空格 1-9 编号落子,对应视图『落子:回复数字 1-9(仅空格)』)。
|
|
11
|
+
*/
|
|
12
|
+
export default defineMiddleware({
|
|
13
|
+
target: 'inbound',
|
|
14
|
+
async handle(context, next) {
|
|
15
|
+
const raw = context.input.content?.trim() ?? '';
|
|
16
|
+
if (!raw) {
|
|
17
|
+
await next();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const services = resolveGameServices(context);
|
|
21
|
+
const message = messageFromCommandInput(context.input);
|
|
22
|
+
const ch = channelKey(message);
|
|
23
|
+
// 直接 payload(QQ 指令预填 / Sandbox action→text)
|
|
24
|
+
const payload = resolveGameTextPayload(raw);
|
|
25
|
+
if (payload?.startsWith(`${TTT_PREFIX}:`)) {
|
|
26
|
+
const restart = parseChoicePayload(payload, TTT_PREFIX);
|
|
27
|
+
if (restart?.choiceId === 'restart') {
|
|
28
|
+
const session = await services.session.getById(restart.sessionId);
|
|
29
|
+
if (session?.channel_key === ch) {
|
|
30
|
+
const reply = await restartFromTerminal(services, message, restart.sessionId);
|
|
31
|
+
if (reply)
|
|
32
|
+
await context.input.$reply(reply);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const move = parseTttPayload(payload);
|
|
37
|
+
if (move) {
|
|
38
|
+
const session = await services.session.getById(move.sessionId);
|
|
39
|
+
if (session?.channel_key === ch) {
|
|
40
|
+
const reply = await handleMove(services, message, move.sessionId, move.cell);
|
|
41
|
+
if (reply)
|
|
42
|
+
await context.input.$reply(reply);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await next();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// 裸数字:按当前棋盘空格编号映射落子
|
|
50
|
+
const session = await services.session.getActiveForUser(ch, message.$sender.id);
|
|
51
|
+
if (!session || session.channel_key !== ch) {
|
|
52
|
+
await next();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const map = buildFallbackMap(session.id, parseBoard(session.board));
|
|
56
|
+
const payloadFromDigit = resolveGameTextPayload(raw, map);
|
|
57
|
+
const move = payloadFromDigit ? parseTttPayload(payloadFromDigit) : null;
|
|
58
|
+
if (!move || move.sessionId !== session.id) {
|
|
59
|
+
await next();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const reply = await handleMove(services, message, session.id, move.cell);
|
|
63
|
+
if (reply)
|
|
64
|
+
await context.input.$reply(reply);
|
|
65
|
+
},
|
|
66
|
+
});
|
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.5",
|
|
4
4
|
"description": "Zhin.js 跨平台井字棋 (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"plugin.
|
|
17
|
+
"plugin.js",
|
|
18
18
|
"schema.json",
|
|
19
19
|
"commands",
|
|
20
20
|
"middlewares",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zhin.js/command": "1.0.
|
|
28
|
-
"@zhin.js/core": "1.4.
|
|
29
|
-
"@zhin.js/game-kit": "2.0.
|
|
30
|
-
"@zhin.js/middleware": "1.0.
|
|
27
|
+
"@zhin.js/command": "1.0.4",
|
|
28
|
+
"@zhin.js/core": "1.4.2",
|
|
29
|
+
"@zhin.js/game-kit": "2.0.2",
|
|
30
|
+
"@zhin.js/middleware": "1.0.4",
|
|
31
31
|
"@zhin.js/plugin-runtime": "1.1.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"zhin": {
|
|
50
50
|
"protocol": 1,
|
|
51
51
|
"type": "plugin",
|
|
52
|
-
"entry": "./plugin.
|
|
52
|
+
"entry": "./plugin.js",
|
|
53
53
|
"engine": "^1.0.0",
|
|
54
54
|
"runtime": "trusted",
|
|
55
55
|
"features": [
|
package/plugin.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineGamePlugin, gameEvents, plainTextFromSendContent, } from '@zhin.js/game-kit';
|
|
3
|
+
import { outboundHostToken } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { defineHostTables } from "./lib/models.js";
|
|
5
|
+
import { gameServicesToken } from "./lib/runtime-store.js";
|
|
6
|
+
import { renderBoard } from "./lib/game-flow.js";
|
|
7
|
+
import { formatTurnStatus, formatWinHeadline, } from "./lib/player-label.js";
|
|
8
|
+
import { createServices, } from "./lib/session-service.js";
|
|
9
|
+
/**
|
|
10
|
+
* Plugin Runtime:
|
|
11
|
+
* - Commands under `commands/` are authoritative (help + bot/join/…).
|
|
12
|
+
* - DB: prefer databaseHostToken; else in-memory SessionServices.
|
|
13
|
+
* - Choice middleware under `middlewares/` handles ttt grid / restart payloads (Sandbox action→text).
|
|
14
|
+
*/
|
|
15
|
+
export default defineGamePlugin({
|
|
16
|
+
name: 'tic-tac-toe',
|
|
17
|
+
metadata: {
|
|
18
|
+
displayName: 'Tic Tac Toe',
|
|
19
|
+
},
|
|
20
|
+
game: {
|
|
21
|
+
id: 'ttt',
|
|
22
|
+
title: '井字棋',
|
|
23
|
+
icon: '♟️',
|
|
24
|
+
description: '三子连珠,群聊排队或人机对战',
|
|
25
|
+
commandPrefix: '/井字棋',
|
|
26
|
+
quickStart: '人机',
|
|
27
|
+
aliases: ['ttt'],
|
|
28
|
+
menus: [
|
|
29
|
+
{ id: 'bot', label: '🤖 人机对战' },
|
|
30
|
+
{ id: 'join', label: '👥 加入排队' },
|
|
31
|
+
{ id: 'spectate', label: '👀 观战' },
|
|
32
|
+
{ id: 'help', label: '📖 玩法说明' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
tables: ['ttt_sessions', 'ttt_queue', 'ttt_moves', 'ttt_spectators'],
|
|
36
|
+
servicesToken: gameServicesToken,
|
|
37
|
+
defineHostTables,
|
|
38
|
+
createServices: (database) => createServices(database),
|
|
39
|
+
session: (services) => services.session,
|
|
40
|
+
setup(context, services) {
|
|
41
|
+
if (!context.resources.has(outboundHostToken))
|
|
42
|
+
return;
|
|
43
|
+
const outbound = context.resources.use(outboundHostToken);
|
|
44
|
+
const notify = async (raw, terminal) => {
|
|
45
|
+
const session = raw;
|
|
46
|
+
if (!session.adapter || !session.endpoint)
|
|
47
|
+
return;
|
|
48
|
+
const spectators = await services.session.listSpectators(session.id);
|
|
49
|
+
if (spectators.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const status = session.status === 'draw'
|
|
52
|
+
? '平局。'
|
|
53
|
+
: terminal && (session.winner === 1 || session.winner === 2)
|
|
54
|
+
? formatWinHeadline(session, session.winner)
|
|
55
|
+
: formatTurnStatus(session, session.move_count);
|
|
56
|
+
const content = plainTextFromSendContent(renderBoard({
|
|
57
|
+
$adapter: session.adapter,
|
|
58
|
+
$endpoint: session.endpoint,
|
|
59
|
+
$channel: { type: 'private', id: '' },
|
|
60
|
+
$sender: { id: '' },
|
|
61
|
+
}, session, status, terminal));
|
|
62
|
+
await Promise.all(spectators.map((userId) => outbound.send({
|
|
63
|
+
adapter: session.adapter,
|
|
64
|
+
endpointId: session.endpoint,
|
|
65
|
+
channelType: 'private',
|
|
66
|
+
channelId: userId,
|
|
67
|
+
content,
|
|
68
|
+
})));
|
|
69
|
+
};
|
|
70
|
+
const disposeTurn = gameEvents.on('turn:change', async (event) => {
|
|
71
|
+
if (event.gameId === 'ttt' && event.session.status === 'active') {
|
|
72
|
+
await notify(event.session, false);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const disposeEnd = gameEvents.on('game:end', async (event) => {
|
|
76
|
+
if (event.gameId === 'ttt')
|
|
77
|
+
await notify(event.session, true);
|
|
78
|
+
});
|
|
79
|
+
return () => {
|
|
80
|
+
disposeEnd();
|
|
81
|
+
disposeTurn();
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
package/src/game-flow.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { GameMessageLike, GameReply } from '@zhin.js/game-kit';
|
|
2
2
|
import type { TttSessionRow } from './models.js';
|
|
3
3
|
import { buildBoardInteractive } from './board-view.js';
|
|
4
4
|
import {
|
|
@@ -31,18 +31,14 @@ export function isBotSession(session: TttSessionRow): boolean {
|
|
|
31
31
|
return session.player_o === BOT_ID || session.player_x === BOT_ID;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
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(
|
|
34
|
+
/** Render the canonical board reply; delivery belongs to the Runtime Host. */
|
|
35
|
+
export function renderBoard(
|
|
40
36
|
message: GameMessageLike,
|
|
41
37
|
session: TttSessionRow,
|
|
42
38
|
statusLine: string,
|
|
43
39
|
terminal = false,
|
|
44
40
|
highlight?: number[],
|
|
45
|
-
):
|
|
41
|
+
): GameReply {
|
|
46
42
|
const board = parseBoard(session.board);
|
|
47
43
|
const content = buildBoardInteractive({
|
|
48
44
|
sessionId: session.id,
|
|
@@ -55,14 +51,14 @@ export function sendOrEditBoard(
|
|
|
55
51
|
channelType: message.$channel.type,
|
|
56
52
|
});
|
|
57
53
|
|
|
58
|
-
return
|
|
54
|
+
return content;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
export async function restartFromTerminal(
|
|
62
58
|
services: SessionServices,
|
|
63
59
|
message: GameMessageLike,
|
|
64
60
|
sessionId: string,
|
|
65
|
-
): Promise<
|
|
61
|
+
): Promise<GameReply | null> {
|
|
66
62
|
const session = await services.session.getById(sessionId);
|
|
67
63
|
if (!session) return '对局不存在。';
|
|
68
64
|
if (session.status === 'active') return '对局尚未结束,无法重开。';
|
|
@@ -80,11 +76,11 @@ export async function restartFromTerminal(
|
|
|
80
76
|
{ id: session.player_x, displayName: session.player_x_name },
|
|
81
77
|
{ id: session.player_o, displayName: session.player_o_name },
|
|
82
78
|
);
|
|
83
|
-
if (board.includes('已有')) return board;
|
|
79
|
+
if (typeof board === 'string' && board.includes('已有')) return board;
|
|
84
80
|
return board;
|
|
85
81
|
}
|
|
86
82
|
const hint = await startBotGame(services, message);
|
|
87
|
-
if (hint.includes('已有')) return hint;
|
|
83
|
+
if (typeof hint === 'string' && hint.includes('已有')) return hint;
|
|
88
84
|
return hint;
|
|
89
85
|
}
|
|
90
86
|
|
|
@@ -101,7 +97,7 @@ export async function handleMove(
|
|
|
101
97
|
message: GameMessageLike,
|
|
102
98
|
sessionId: string,
|
|
103
99
|
cell: number,
|
|
104
|
-
): Promise<
|
|
100
|
+
): Promise<GameReply | null> {
|
|
105
101
|
const session = await services.session.getById(sessionId);
|
|
106
102
|
if (!session || session.status !== 'active') {
|
|
107
103
|
return '对局不存在或已结束。';
|
|
@@ -144,35 +140,12 @@ export async function handleMove(
|
|
|
144
140
|
|
|
145
141
|
const updated = (await services.session.getById(session.id))!;
|
|
146
142
|
|
|
147
|
-
if (win || draw) {
|
|
148
|
-
const humanId = isBotSession(updated)
|
|
149
|
-
? (updated.player_x === BOT_ID ? updated.player_o : updated.player_x)
|
|
150
|
-
: message.$sender.id;
|
|
151
|
-
const humanName = isBotSession(updated)
|
|
152
|
-
? (updated.player_x === BOT_ID ? updated.player_o_name : updated.player_x_name)
|
|
153
|
-
: String(message.$sender.name ?? message.$sender.id);
|
|
154
|
-
const humanMsg = {
|
|
155
|
-
...message,
|
|
156
|
-
$sender: { ...message.$sender, id: humanId, name: humanName },
|
|
157
|
-
} as GameMessageLike;
|
|
158
|
-
if (win) {
|
|
159
|
-
const humanMark = playerMark(humanId, {
|
|
160
|
-
playerX: updated.player_x,
|
|
161
|
-
playerO: updated.player_o,
|
|
162
|
-
});
|
|
163
|
-
const humanWon = humanMark === win.winner;
|
|
164
|
-
void recordGameOutcome(humanMsg, 'ttt', humanWon ? 'won' : 'lost', humanWon ? 20 : 0);
|
|
165
|
-
} else {
|
|
166
|
-
void recordGameOutcome(humanMsg, 'ttt', 'draw');
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
143
|
if (win) {
|
|
171
144
|
const status = formatWinHeadline(updated, win.winner);
|
|
172
|
-
return
|
|
145
|
+
return renderBoard(message, updated, status, true, win.line);
|
|
173
146
|
}
|
|
174
147
|
if (draw) {
|
|
175
|
-
return
|
|
148
|
+
return renderBoard(message, updated, '平局。', true);
|
|
176
149
|
}
|
|
177
150
|
|
|
178
151
|
// 人机:玩家落子后立刻由服务端代下,避免先发「轮到机器人」再发终盘(QQ 被动消息多耗一次)
|
|
@@ -181,14 +154,14 @@ export async function handleMove(
|
|
|
181
154
|
}
|
|
182
155
|
|
|
183
156
|
const status = formatTurnStatus(updated, moveCount);
|
|
184
|
-
return
|
|
157
|
+
return renderBoard(message, updated, status, false);
|
|
185
158
|
}
|
|
186
159
|
|
|
187
160
|
async function runBotMove(
|
|
188
161
|
services: SessionServices,
|
|
189
162
|
message: GameMessageLike,
|
|
190
163
|
session: TttSessionRow,
|
|
191
|
-
): Promise<
|
|
164
|
+
): Promise<GameReply | null> {
|
|
192
165
|
const board = parseBoard(session.board);
|
|
193
166
|
const aiMark = session.player_o === BOT_ID ? O : X;
|
|
194
167
|
const cell = bestMove(board, aiMark);
|
|
@@ -205,7 +178,7 @@ async function runBotMove(
|
|
|
205
178
|
export async function startBotGame(
|
|
206
179
|
services: SessionServices,
|
|
207
180
|
message: GameMessageLike,
|
|
208
|
-
): Promise<
|
|
181
|
+
): Promise<GameReply> {
|
|
209
182
|
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
210
183
|
const active = await services.session.getActiveByChannel(ch);
|
|
211
184
|
if (active) return '当前频道已有进行中的对局。';
|
|
@@ -220,8 +193,9 @@ export async function startBotGame(
|
|
|
220
193
|
});
|
|
221
194
|
|
|
222
195
|
const status = `${formatRosterLine(session)} · 你先手 (✕)`;
|
|
223
|
-
const
|
|
224
|
-
|
|
196
|
+
const board = renderBoard(message, session, status, false);
|
|
197
|
+
const success = '\n\n开局成功!点击空格或回复数字 1–9 落子。';
|
|
198
|
+
return Array.isArray(board) ? [...board, success] : [board, success];
|
|
225
199
|
}
|
|
226
200
|
|
|
227
201
|
export async function startPvpGame(
|
|
@@ -229,7 +203,7 @@ export async function startPvpGame(
|
|
|
229
203
|
message: GameMessageLike,
|
|
230
204
|
playerX: TttPlayerRef,
|
|
231
205
|
playerO: TttPlayerRef,
|
|
232
|
-
): Promise<
|
|
206
|
+
): Promise<GameReply> {
|
|
233
207
|
const ch = `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
234
208
|
const active = await services.session.getActiveByChannel(ch);
|
|
235
209
|
if (active) return '当前频道已有进行中的对局。';
|
|
@@ -244,5 +218,5 @@ export async function startPvpGame(
|
|
|
244
218
|
});
|
|
245
219
|
const opener = formatPlayerWithMark(session.player_x, session.player_x_name, '✕');
|
|
246
220
|
const status = `${formatRosterLine(session)} · 先手:${opener}`;
|
|
247
|
-
return
|
|
221
|
+
return renderBoard(message, session, status, false);
|
|
248
222
|
}
|
package/src/index.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -19,7 +19,6 @@ declare module '@zhin.js/core' {
|
|
|
19
19
|
turn: number;
|
|
20
20
|
status: TttSessionStatus;
|
|
21
21
|
winner: number;
|
|
22
|
-
board_message_id: string;
|
|
23
22
|
move_count: number;
|
|
24
23
|
updated_at: number;
|
|
25
24
|
created_at: number;
|
|
@@ -75,7 +74,6 @@ export function defineHostTables(
|
|
|
75
74
|
turn: { type: 'integer', default: 1 },
|
|
76
75
|
status: { type: 'text', default: 'active' },
|
|
77
76
|
winner: { type: 'integer', default: 0 },
|
|
78
|
-
board_message_id: { type: 'text', default: '' },
|
|
79
77
|
move_count: { type: 'integer', default: 0 },
|
|
80
78
|
updated_at: { type: 'integer', default: 0 },
|
|
81
79
|
created_at: { type: 'integer', default: 0 },
|
package/src/session-service.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Database, Models, RelatedModel } from '@zhin.js/core';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BaseSessionService,
|
|
4
|
+
channelKey,
|
|
5
|
+
generateCompactId,
|
|
6
|
+
type GameMessageLike,
|
|
7
|
+
type GameSessionDatabase,
|
|
8
|
+
} from '@zhin.js/game-kit';
|
|
3
9
|
import type { TttModelName, TttSessionRow } from './models.js';
|
|
10
|
+
import { BOT_ID } from './player-label.js';
|
|
4
11
|
|
|
5
12
|
/** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
|
|
6
13
|
export type TttDatabase = Database<unknown, Models, string>;
|
|
@@ -96,36 +103,41 @@ export function sessionId(): string {
|
|
|
96
103
|
return generateCompactId('s');
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
export class SessionService {
|
|
100
|
-
constructor(private readonly db: TttDatabase) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
export class SessionService extends BaseSessionService<TttSessionRow> {
|
|
107
|
+
constructor(private readonly db: TttDatabase) {
|
|
108
|
+
super(db as unknown as GameSessionDatabase<TttSessionRow>, {
|
|
109
|
+
gameId: 'ttt',
|
|
110
|
+
table: 'ttt_sessions',
|
|
111
|
+
userFields: ['player_x', 'player_o'],
|
|
112
|
+
projectOutcomes: (session) => {
|
|
113
|
+
if (session.status !== 'won' && session.status !== 'draw') return [];
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
id: session.player_x,
|
|
117
|
+
name: session.player_x_name,
|
|
118
|
+
mark: 1,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: session.player_o,
|
|
122
|
+
name: session.player_o_name,
|
|
123
|
+
mark: 2,
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
.filter((player) => player.id !== BOT_ID)
|
|
127
|
+
.map((player) => ({
|
|
128
|
+
userId: player.id,
|
|
129
|
+
userName: player.name,
|
|
130
|
+
result: session.status === 'draw'
|
|
131
|
+
? 'draw' as const
|
|
132
|
+
: session.winner === player.mark
|
|
133
|
+
? 'won' as const
|
|
134
|
+
: 'lost' as const,
|
|
135
|
+
score: session.status === 'won' && session.winner === player.mark
|
|
136
|
+
? 20
|
|
137
|
+
: undefined,
|
|
138
|
+
}));
|
|
139
|
+
},
|
|
106
140
|
});
|
|
107
|
-
return rows[0] ?? null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async getById(id: string): Promise<TttSessionRow | null> {
|
|
111
|
-
return getModel(this.db, 'ttt_sessions').findOne({ id });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async getActiveForUser(channel: string, userId: string): Promise<TttSessionRow | null> {
|
|
115
|
-
const row = await this.getActiveByChannel(channel);
|
|
116
|
-
if (!row) return null;
|
|
117
|
-
if (row.player_x === userId || row.player_o === userId) return row;
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 根据棋盘消息 ID 查找进行中的局(QQ 等 action 带 sourceMessageId) */
|
|
122
|
-
async getActiveByBoardMessageId(messageId: string): Promise<TttSessionRow | null> {
|
|
123
|
-
if (!messageId) return null;
|
|
124
|
-
const rows = await getModel(this.db, 'ttt_sessions').findAll({});
|
|
125
|
-
for (const row of rows) {
|
|
126
|
-
if (boardMessageMatches(row.board_message_id ?? '', messageId)) return row;
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
async createSession(input: {
|
|
@@ -154,20 +166,11 @@ export class SessionService {
|
|
|
154
166
|
turn: 1,
|
|
155
167
|
status: 'active',
|
|
156
168
|
winner: 0,
|
|
157
|
-
board_message_id: '',
|
|
158
169
|
move_count: 0,
|
|
159
170
|
updated_at: now,
|
|
160
171
|
created_at: now,
|
|
161
172
|
};
|
|
162
|
-
|
|
163
|
-
return row;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async updateSession(id: string, patch: Partial<TttSessionRow>): Promise<void> {
|
|
167
|
-
await getModel(this.db, 'ttt_sessions').updateWhere(
|
|
168
|
-
{ id },
|
|
169
|
-
{ ...patch, updated_at: Date.now() },
|
|
170
|
-
);
|
|
173
|
+
return this.createRow(row);
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
async recordMove(sessionId: string, playerId: string, cell: number, moveIndex: number): Promise<void> {
|
|
@@ -192,19 +195,6 @@ export class SessionService {
|
|
|
192
195
|
return rows.map((r) => r.user_id);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
async abortStale(idleMs: number): Promise<number> {
|
|
196
|
-
const cutoff = Date.now() - idleMs;
|
|
197
|
-
const sessions = getModel(this.db, 'ttt_sessions');
|
|
198
|
-
const rows = await sessions.findAll({ status: 'active' });
|
|
199
|
-
let n = 0;
|
|
200
|
-
for (const row of rows) {
|
|
201
|
-
if (row.updated_at < cutoff) {
|
|
202
|
-
await sessions.updateWhere({ id: row.id }, { status: 'aborted', updated_at: Date.now() });
|
|
203
|
-
n++;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return n;
|
|
207
|
-
}
|
|
208
198
|
}
|
|
209
199
|
|
|
210
200
|
export type SessionServices = { queue: QueueService; session: SessionService };
|
|
@@ -212,4 +202,3 @@ export type SessionServices = { queue: QueueService; session: SessionService };
|
|
|
212
202
|
export function createServices(db: TttDatabase): SessionServices {
|
|
213
203
|
return { queue: new QueueService(db), session: new SessionService(db) };
|
|
214
204
|
}
|
|
215
|
-
|
package/src/ttt-command.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
channelKey,
|
|
3
|
+
type GameMessageLike,
|
|
4
|
+
type GameReply,
|
|
5
|
+
} from '@zhin.js/game-kit';
|
|
2
6
|
import { formatPlayerWithMark, formatRosterLine } from './player-label.js';
|
|
3
7
|
import { startBotGame, startPvpGame } from './game-flow.js';
|
|
4
8
|
import type { SessionServices } from './session-service.js';
|
|
@@ -17,7 +21,7 @@ export async function runTttCommand(
|
|
|
17
21
|
services: SessionServices,
|
|
18
22
|
message: GameMessageLike,
|
|
19
23
|
action: string,
|
|
20
|
-
): Promise<
|
|
24
|
+
): Promise<GameReply> {
|
|
21
25
|
const ch = channelKey(message);
|
|
22
26
|
const userId = message.$sender.id;
|
|
23
27
|
|
|
@@ -43,7 +47,11 @@ export async function runTttCommand(
|
|
|
43
47
|
const [px, po] = pair;
|
|
44
48
|
const board = await startPvpGame(services, message, px, po);
|
|
45
49
|
const matchLine = `匹配成功!${formatPlayerWithMark(px.id, px.displayName, '✕')} vs ${formatPlayerWithMark(po.id, po.displayName, '○')}`;
|
|
46
|
-
return board
|
|
50
|
+
return board
|
|
51
|
+
? Array.isArray(board)
|
|
52
|
+
? [matchLine, '\n\n', ...board]
|
|
53
|
+
: [matchLine, '\n\n', board]
|
|
54
|
+
: matchLine;
|
|
47
55
|
}
|
|
48
56
|
return `已加入排队(第 ${position} 位),凑满 2 人自动开局。`;
|
|
49
57
|
}
|
package/lib/memory-db.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { DatabaseHost } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { type TttDatabase, type SessionServices } from './session-service.js';
|
|
3
|
-
/** In-memory ttt tables for Plugin Runtime slice-2 (no DatabaseFeature). */
|
|
4
|
-
export declare function createInMemoryTttDb(): TttDatabase;
|
|
5
|
-
export declare function mountTttMemoryServices(): SessionServices;
|
|
6
|
-
export declare function mountTttHostServices(host: DatabaseHost): SessionServices;
|
package/lib/memory-db.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { createHostGameDb, createInMemoryGameDb } from '@zhin.js/game-kit';
|
|
2
|
-
import { defineHostTables } from './models.js';
|
|
3
|
-
import { createServices } from './session-service.js';
|
|
4
|
-
const TTT_TABLES = ['ttt_sessions', 'ttt_queue', 'ttt_moves', 'ttt_spectators'];
|
|
5
|
-
/** In-memory ttt tables for Plugin Runtime slice-2 (no DatabaseFeature). */
|
|
6
|
-
export function createInMemoryTttDb() {
|
|
7
|
-
return createInMemoryGameDb(TTT_TABLES);
|
|
8
|
-
}
|
|
9
|
-
export function mountTttMemoryServices() {
|
|
10
|
-
const services = createServices(createInMemoryTttDb());
|
|
11
|
-
return services;
|
|
12
|
-
}
|
|
13
|
-
export function mountTttHostServices(host) {
|
|
14
|
-
defineHostTables(host);
|
|
15
|
-
const services = createServices(createHostGameDb(host, TTT_TABLES));
|
|
16
|
-
return services;
|
|
17
|
-
}
|
|
18
|
-
//# sourceMappingURL=memory-db.js.map
|
package/lib/memory-db.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"memory-db.js","sourceRoot":"","sources":["../src/memory-db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,cAAc,EAA0C,MAAM,sBAAsB,CAAC;AAE9F,MAAM,UAAU,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,CAAU,CAAC;AAEzF,4EAA4E;AAC5E,MAAM,UAAU,mBAAmB;IACjC,OAAO,oBAAoB,CAAC,UAAU,CAA2B,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,MAAM,QAAQ,GAAG,cAAc,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACvD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAkB;IACrD,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,QAAQ,GAAG,cAAc,CAAC,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAA2B,CAAC,CAAC;IAC9F,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/plugin.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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/src/memory-db.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
}
|