koishi-plugin-play-go-chess 0.1.0

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.
@@ -0,0 +1,23 @@
1
+ import { CellState, PlayerColor, Position, MoveResult } from './types';
2
+ /**
3
+ * 获取包含 pos 的连通同色棋组 (BFS)
4
+ */
5
+ export declare function getGroup(board: CellState[][], pos: Position): Position[];
6
+ /**
7
+ * 获取棋组的所有气 (唯一空邻点)
8
+ */
9
+ export declare function getLiberties(board: CellState[][], group: Position[]): Position[];
10
+ /**
11
+ * 围棋落子核心逻辑
12
+ * 返回新棋盘状态(不修改原棋盘)、提子列表、是否有效、终局信息
13
+ */
14
+ export declare function applyMove(board: CellState[][], pos: Position, color: PlayerColor, boardHistory?: string[]): MoveResult;
15
+ /**
16
+ * 中国数子法计分
17
+ * 计算黑白双方在棋盘上的子+所围空的总数
18
+ */
19
+ export declare function calculateScore(board: CellState[][], capturedByBlack: number, capturedByWhite: number, komi: number): {
20
+ black: number;
21
+ white: number;
22
+ winner: PlayerColor | 'draw';
23
+ };
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getGroup = getGroup;
4
+ exports.getLiberties = getLiberties;
5
+ exports.applyMove = applyMove;
6
+ exports.calculateScore = calculateScore;
7
+ const utils_1 = require("./utils");
8
+ /**
9
+ * 获取包含 pos 的连通同色棋组 (BFS)
10
+ */
11
+ function getGroup(board, pos) {
12
+ const size = board.length;
13
+ const color = board[pos.row][pos.col];
14
+ if (!color)
15
+ return [];
16
+ const group = [];
17
+ const visited = new Set();
18
+ const queue = [pos];
19
+ while (queue.length > 0) {
20
+ const current = queue.shift();
21
+ const key = `${current.row},${current.col}`;
22
+ if (visited.has(key))
23
+ continue;
24
+ visited.add(key);
25
+ group.push(current);
26
+ for (const [dr, dc] of utils_1.FOUR_DIRECTIONS) {
27
+ const nr = current.row + dr;
28
+ const nc = current.col + dc;
29
+ if (nr >= 0 && nr < size && nc >= 0 && nc < size && board[nr][nc] === color) {
30
+ const nKey = `${nr},${nc}`;
31
+ if (!visited.has(nKey)) {
32
+ queue.push({ row: nr, col: nc });
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return group;
38
+ }
39
+ /**
40
+ * 获取棋组的所有气 (唯一空邻点)
41
+ */
42
+ function getLiberties(board, group) {
43
+ const size = board.length;
44
+ const liberties = [];
45
+ const seen = new Set();
46
+ for (const stone of group) {
47
+ for (const [dr, dc] of utils_1.FOUR_DIRECTIONS) {
48
+ const nr = stone.row + dr;
49
+ const nc = stone.col + dc;
50
+ if (nr >= 0 && nr < size && nc >= 0 && nc < size && board[nr][nc] === null) {
51
+ const key = `${nr},${nc}`;
52
+ if (!seen.has(key)) {
53
+ seen.add(key);
54
+ liberties.push({ row: nr, col: nc });
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return liberties;
60
+ }
61
+ /**
62
+ * 围棋落子核心逻辑
63
+ * 返回新棋盘状态(不修改原棋盘)、提子列表、是否有效、终局信息
64
+ */
65
+ function applyMove(board, pos, color, boardHistory) {
66
+ const size = board.length;
67
+ if (!(0, utils_1.isValidPosition)(pos, size)) {
68
+ return { valid: false, message: '坐标超出棋盘范围,请重新输入。' };
69
+ }
70
+ if (board[pos.row][pos.col] !== null) {
71
+ return { valid: false, message: '此交叉点已有棋子,请重新落子。' };
72
+ }
73
+ // 在副本上操作
74
+ const newBoard = (0, utils_1.cloneBoard)(board);
75
+ newBoard[pos.row][pos.col] = color;
76
+ const opponent = (0, utils_1.oppositeColor)(color);
77
+ const captured = [];
78
+ // 1. 检查并提走对方死棋
79
+ for (const [dr, dc] of utils_1.FOUR_DIRECTIONS) {
80
+ const nr = pos.row + dr;
81
+ const nc = pos.col + dc;
82
+ if (nr < 0 || nr >= size || nc < 0 || nc >= size)
83
+ continue;
84
+ if (newBoard[nr][nc] !== opponent)
85
+ continue;
86
+ const opponentGroup = getGroup(newBoard, { row: nr, col: nc });
87
+ const liberties = getLiberties(newBoard, opponentGroup);
88
+ if (liberties.length === 0) {
89
+ // 提子
90
+ for (const stone of opponentGroup) {
91
+ newBoard[stone.row][stone.col] = null;
92
+ captured.push(stone);
93
+ }
94
+ }
95
+ }
96
+ // 2. 检查己方是否有气 (禁着/自杀检测)
97
+ const ownGroup = getGroup(newBoard, pos);
98
+ const ownLiberties = getLiberties(newBoard, ownGroup);
99
+ if (ownLiberties.length === 0) {
100
+ return { valid: false, message: '禁入点:落子后己方无气(自杀禁着),请重新落子。' };
101
+ }
102
+ // 3. 劫争检测
103
+ if (boardHistory && isSuperko(boardHistory, newBoard)) {
104
+ return { valid: false, message: '劫争:此局面先前已出现过(全局同型禁止再现),请找劫材后重新落子。' };
105
+ }
106
+ return {
107
+ valid: true,
108
+ captured: captured.length > 0 ? captured : undefined,
109
+ };
110
+ }
111
+ /**
112
+ * 超级劫检测: 检查当前棋盘局面是否在历史中出现过
113
+ */
114
+ function isSuperko(boardHistory, board) {
115
+ const serialized = (0, utils_1.serializeBoard)(board);
116
+ return boardHistory.includes(serialized);
117
+ }
118
+ /**
119
+ * 中国数子法计分
120
+ * 计算黑白双方在棋盘上的子+所围空的总数
121
+ */
122
+ function calculateScore(board, capturedByBlack, capturedByWhite, komi) {
123
+ const size = board.length;
124
+ // 每个空点归属于接触的颜色 (只接触一种颜色则为该色的空; 接触两种则为中立)
125
+ const visited = new Set();
126
+ let blackTerritory = 0;
127
+ let whiteTerritory = 0;
128
+ let blackStones = 0;
129
+ let whiteStones = 0;
130
+ // 先数子
131
+ for (let r = 0; r < size; r++) {
132
+ for (let c = 0; c < size; c++) {
133
+ if (board[r][c] === 'black')
134
+ blackStones++;
135
+ else if (board[r][c] === 'white')
136
+ whiteStones++;
137
+ }
138
+ }
139
+ // 再数空 (对每个空区域进行泛洪)
140
+ for (let r = 0; r < size; r++) {
141
+ for (let c = 0; c < size; c++) {
142
+ if (board[r][c] !== null)
143
+ continue;
144
+ const key = `${r},${c}`;
145
+ if (visited.has(key))
146
+ continue;
147
+ // 泛洪此空区域
148
+ const region = floodFillEmpty(board, { row: r, col: c });
149
+ for (const p of region) {
150
+ visited.add(`${p.row},${p.col}`);
151
+ }
152
+ // 判断区域归属
153
+ let touchesBlack = false;
154
+ let touchesWhite = false;
155
+ for (const p of region) {
156
+ for (const [dr, dc] of utils_1.FOUR_DIRECTIONS) {
157
+ const nr = p.row + dr;
158
+ const nc = p.col + dc;
159
+ if (nr < 0 || nr >= size || nc < 0 || nc >= size)
160
+ continue;
161
+ if (board[nr][nc] === 'black')
162
+ touchesBlack = true;
163
+ if (board[nr][nc] === 'white')
164
+ touchesWhite = true;
165
+ }
166
+ }
167
+ if (touchesBlack && !touchesWhite) {
168
+ blackTerritory += region.length;
169
+ }
170
+ else if (touchesWhite && !touchesBlack) {
171
+ whiteTerritory += region.length;
172
+ }
173
+ // 双活区域不计入任何一方
174
+ }
175
+ }
176
+ // 中国规则: 子空皆地
177
+ const blackScore = blackStones + blackTerritory;
178
+ const whiteScore = whiteStones + whiteTerritory + komi;
179
+ let winner;
180
+ if (blackScore > whiteScore) {
181
+ winner = 'black';
182
+ }
183
+ else if (whiteScore > blackScore) {
184
+ winner = 'white';
185
+ }
186
+ else {
187
+ winner = 'draw';
188
+ }
189
+ return { black: blackScore, white: whiteScore, winner };
190
+ }
191
+ /**
192
+ * 泛洪填充一个空区域
193
+ */
194
+ function floodFillEmpty(board, start) {
195
+ const size = board.length;
196
+ const region = [];
197
+ const visited = new Set();
198
+ const queue = [start];
199
+ while (queue.length > 0) {
200
+ const current = queue.shift();
201
+ const key = `${current.row},${current.col}`;
202
+ if (visited.has(key))
203
+ continue;
204
+ visited.add(key);
205
+ region.push(current);
206
+ for (const [dr, dc] of utils_1.FOUR_DIRECTIONS) {
207
+ const nr = current.row + dr;
208
+ const nc = current.col + dc;
209
+ if (nr >= 0 && nr < size && nc >= 0 && nc < size && board[nr][nc] === null) {
210
+ const nKey = `${nr},${nc}`;
211
+ if (!visited.has(nKey)) {
212
+ queue.push({ row: nr, col: nc });
213
+ }
214
+ }
215
+ }
216
+ }
217
+ return region;
218
+ }
@@ -0,0 +1,15 @@
1
+ import { CellState, PlayerColor, Position, MoveResult } from './types';
2
+ /**
3
+ * 检查指定颜色是否形成五连
4
+ * 从 lastPos 开始向四个方向扫描
5
+ */
6
+ export declare function checkWin(board: CellState[][], color: PlayerColor, lastPos?: Position): boolean;
7
+ /**
8
+ * 检测有禁手模式下黑方的禁手
9
+ * 返回禁手类型描述,或 null 表示不是禁手
10
+ */
11
+ export declare function isForbiddenMove(board: CellState[][], pos: Position): string | null;
12
+ /**
13
+ * 五子棋落子 (合并检查逻辑)
14
+ */
15
+ export declare function applyGomokuMove(board: CellState[][], pos: Position, color: PlayerColor, mode: 'free' | 'restricted'): MoveResult;
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkWin = checkWin;
4
+ exports.isForbiddenMove = isForbiddenMove;
5
+ exports.applyGomokuMove = applyGomokuMove;
6
+ const utils_1 = require("./utils");
7
+ /**
8
+ * 检查指定颜色是否形成五连
9
+ * 从 lastPos 开始向四个方向扫描
10
+ */
11
+ function checkWin(board, color, lastPos) {
12
+ const size = board.length;
13
+ const directions = [
14
+ [0, 1], // 水平
15
+ [1, 0], // 垂直
16
+ [1, 1], // 对角线 ↘
17
+ [1, -1], // 对角线 ↙
18
+ ];
19
+ const checkPos = lastPos || findLastMove(board, color);
20
+ if (!checkPos) {
21
+ // 全盘扫描 (仅在无 lastPos 时,用于初始化后)
22
+ for (let r = 0; r < size; r++) {
23
+ for (let c = 0; c < size; c++) {
24
+ if (board[r][c] !== color)
25
+ continue;
26
+ for (const [dr, dc] of directions) {
27
+ if (countConsecutive(board, { row: r, col: c }, dr, dc, color) >= 5) {
28
+ return true;
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ // 从最后落子位置检查
36
+ for (const [dr, dc] of directions) {
37
+ const count = countConsecutive(board, checkPos, dr, dc, color);
38
+ if (count >= 5)
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ /**
44
+ * 查找棋盘上最后一个同色棋子(用于无 lastPos 时的扫描起点)
45
+ */
46
+ function findLastMove(board, color) {
47
+ // 简化: 返回第一个找到的该色棋子
48
+ for (let r = 0; r < board.length; r++) {
49
+ for (let c = 0; c < board.length; c++) {
50
+ if (board[r][c] === color)
51
+ return { row: r, col: c };
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * 从 pos 开始沿 (dr, dc) 方向计数连续同色棋子 (包括 pos 本身)
58
+ * 向正反两个方向计数
59
+ */
60
+ function countConsecutive(board, pos, dr, dc, color) {
61
+ const size = board.length;
62
+ let count = 1; // 包括 pos 本身
63
+ // 正方向
64
+ let r = pos.row + dr;
65
+ let c = pos.col + dc;
66
+ while (r >= 0 && r < size && c >= 0 && c < size && board[r][c] === color) {
67
+ count++;
68
+ r += dr;
69
+ c += dc;
70
+ }
71
+ // 反方向
72
+ r = pos.row - dr;
73
+ c = pos.col - dc;
74
+ while (r >= 0 && r < size && c >= 0 && c < size && board[r][c] === color) {
75
+ count++;
76
+ r -= dr;
77
+ c -= dc;
78
+ }
79
+ return count;
80
+ }
81
+ /**
82
+ * 检测有禁手模式下黑方的禁手
83
+ * 返回禁手类型描述,或 null 表示不是禁手
84
+ */
85
+ function isForbiddenMove(board, pos) {
86
+ const size = board.length;
87
+ // 在 pos 处模拟放置黑子
88
+ const simBoard = (0, utils_1.cloneBoard)(board);
89
+ simBoard[pos.row][pos.col] = 'black';
90
+ // 1. 检查长连 (超过 5 子)
91
+ if (hasOverline(simBoard, pos)) {
92
+ return '长连禁手:落子后形成六子或更长连线,请重新落子。';
93
+ }
94
+ // 2. 检查五连 — 五连不是禁手 (允许获胜)
95
+ if (checkWin(simBoard, 'black', pos)) {
96
+ return null;
97
+ }
98
+ // 3. 检查双四
99
+ const fourCount = countFours(simBoard, pos);
100
+ if (fourCount >= 2) {
101
+ return '双四禁手:一子同时形成两个四,请重新落子。';
102
+ }
103
+ // 4. 检查双三
104
+ const threeCount = countOpenThrees(simBoard, pos);
105
+ if (threeCount >= 2) {
106
+ return '双三禁手:一子同时形成两个活三,请重新落子。';
107
+ }
108
+ return null;
109
+ }
110
+ /**
111
+ * 检查从 pos 开始是否有长连 (超过 5)
112
+ */
113
+ function hasOverline(board, pos) {
114
+ const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
115
+ for (const [dr, dc] of directions) {
116
+ if (countConsecutive(board, pos, dr, dc, 'black') > 5) {
117
+ return true;
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+ /**
123
+ * 统计 pos 处的冲四/活四数量
124
+ * 四: 4 子连续且至少一端开放
125
+ */
126
+ function countFours(board, pos) {
127
+ const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
128
+ let count = 0;
129
+ for (const [dr, dc] of directions) {
130
+ if (isFour(board, pos, dr, dc)) {
131
+ count++;
132
+ }
133
+ }
134
+ return count;
135
+ }
136
+ /**
137
+ * 检查在 (dr, dc) 方向上是否形成四
138
+ */
139
+ function isFour(board, pos, dr, dc) {
140
+ const size = board.length;
141
+ const consecutive = countConsecutive(board, pos, dr, dc, 'black');
142
+ if (consecutive !== 4)
143
+ return false;
144
+ // 检查两端: 至少一端为空
145
+ const forwardR = pos.row + dr * consecutive; // 实际上应该是 +dr * forwardCount
146
+ const forwardC = pos.col + dc * consecutive;
147
+ // 重新计算: 找到整个连续段的两端
148
+ let startR = pos.row;
149
+ let startC = pos.col;
150
+ while (startR - dr >= 0 && startR - dr < size && startC - dc >= 0 && startC - dc < size && board[startR - dr][startC - dc] === 'black') {
151
+ startR -= dr;
152
+ startC -= dc;
153
+ }
154
+ let endR = pos.row;
155
+ let endC = pos.col;
156
+ while (endR + dr >= 0 && endR + dr < size && endC + dc >= 0 && endC + dc < size && board[endR + dr][endC + dc] === 'black') {
157
+ endR += dr;
158
+ endC += dc;
159
+ }
160
+ // 检查两端是否为空
161
+ const beforeEmpty = (0, utils_1.isValidPosition)({ row: startR - dr, col: startC - dc }, size)
162
+ && board[startR - dr][startC - dc] === null;
163
+ const afterEmpty = (0, utils_1.isValidPosition)({ row: endR + dr, col: endC + dc }, size)
164
+ && board[endR + dr][endC + dc] === null;
165
+ return beforeEmpty || afterEmpty;
166
+ }
167
+ /**
168
+ * 统计 pos 处的活三数量
169
+ * 活三: 3 子连续,两端都开放,且延伸后能形成活四
170
+ */
171
+ function countOpenThrees(board, pos) {
172
+ const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
173
+ let count = 0;
174
+ for (const [dr, dc] of directions) {
175
+ if (isOpenThree(board, pos, dr, dc)) {
176
+ count++;
177
+ }
178
+ }
179
+ return count;
180
+ }
181
+ /**
182
+ * 检查在 (dr, dc) 方向上是否形成活三
183
+ */
184
+ function isOpenThree(board, pos, dr, dc) {
185
+ const size = board.length;
186
+ // 找到包含 pos 的连续黑子段
187
+ let startR = pos.row;
188
+ let startC = pos.col;
189
+ while (startR - dr >= 0 && startR - dr < size && startC - dc >= 0 && startC - dc < size && board[startR - dr][startC - dc] === 'black') {
190
+ startR -= dr;
191
+ startC -= dc;
192
+ }
193
+ let endR = pos.row;
194
+ let endC = pos.col;
195
+ while (endR + dr >= 0 && endR + dr < size && endC + dc >= 0 && endC + dc < size && board[endR + dr][endC + dc] === 'black') {
196
+ endR += dr;
197
+ endC += dc;
198
+ }
199
+ const seqLen = Math.max(Math.abs(endR - startR), Math.abs(endC - startC)) + 1;
200
+ if (seqLen !== 3)
201
+ return false;
202
+ // 两端必须都为空
203
+ const beforeR = startR - dr;
204
+ const beforeC = startC - dc;
205
+ const afterR = endR + dr;
206
+ const afterC = endC + dc;
207
+ if (!(0, utils_1.isValidPosition)({ row: beforeR, col: beforeC }, size) || board[beforeR][beforeC] !== null)
208
+ return false;
209
+ if (!(0, utils_1.isValidPosition)({ row: afterR, col: afterC }, size) || board[afterR][afterC] !== null)
210
+ return false;
211
+ // 进一步检查: 两端延伸一格后不是对方棋子 (确保是真正的活三)
212
+ const beyondBeforeR = beforeR - dr;
213
+ const beyondBeforeC = beforeC - dc;
214
+ const beyondAfterR = afterR + dr;
215
+ const beyondAfterC = afterC + dc;
216
+ const beforeBlocked = !(0, utils_1.isValidPosition)({ row: beyondBeforeR, col: beyondBeforeC }, size)
217
+ || board[beyondBeforeR][beyondBeforeC] === 'white';
218
+ const afterBlocked = !(0, utils_1.isValidPosition)({ row: beyondAfterR, col: beyondAfterC }, size)
219
+ || board[beyondAfterR][beyondAfterC] === 'white';
220
+ // 活三要求两端都不被堵死
221
+ return !beforeBlocked || !afterBlocked;
222
+ }
223
+ /**
224
+ * 五子棋落子 (合并检查逻辑)
225
+ */
226
+ function applyGomokuMove(board, pos, color, mode) {
227
+ const size = board.length;
228
+ if (!(0, utils_1.isValidPosition)(pos, size)) {
229
+ return { valid: false, message: '坐标超出棋盘范围,请重新输入。' };
230
+ }
231
+ if (board[pos.row][pos.col] !== null) {
232
+ return { valid: false, message: '此交叉点已有棋子,请重新落子。' };
233
+ }
234
+ // 有禁手模式:检查黑方禁手
235
+ if (mode === 'restricted' && color === 'black') {
236
+ const forbidden = isForbiddenMove(board, pos);
237
+ if (forbidden) {
238
+ return { valid: false, message: forbidden };
239
+ }
240
+ }
241
+ // 应用落子
242
+ const newBoard = (0, utils_1.cloneBoard)(board);
243
+ newBoard[pos.row][pos.col] = color;
244
+ // 检查是否获胜
245
+ const isWin = checkWin(newBoard, color, pos);
246
+ return {
247
+ valid: true,
248
+ gameOver: isWin,
249
+ winner: isWin ? color : undefined,
250
+ };
251
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "play-go-chess";
3
+ export declare const inject: readonly ["database", "canvas"];
4
+ export interface Config {
5
+ commandStart: string;
6
+ commandPlace: string;
7
+ commandPass: string;
8
+ commandUndo: string;
9
+ commandResign: string;
10
+ commandEnd: string;
11
+ commandView: string;
12
+ commandStats: string;
13
+ goKomi: number;
14
+ }
15
+ export declare const Config: Schema<Config>;
16
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Config = exports.inject = exports.name = void 0;
37
+ exports.apply = apply;
38
+ const koishi_1 = require("koishi");
39
+ exports.name = 'play-go-chess';
40
+ exports.inject = ['database', 'canvas'];
41
+ exports.Config = koishi_1.Schema.object({
42
+ commandStart: koishi_1.Schema.string().default('下棋').description('开始/创建棋局'),
43
+ commandPlace: koishi_1.Schema.string().default('落子').description('落子(非开局者落子时自动加入对局)'),
44
+ commandPass: koishi_1.Schema.string().default('停一手').description('停着(围棋/黑白棋,也可用于加入对局)'),
45
+ commandUndo: koishi_1.Schema.string().default('悔棋').description('悔棋(回退一步,可多次使用)'),
46
+ commandResign: koishi_1.Schema.string().default('认输').description('认输'),
47
+ commandEnd: koishi_1.Schema.string().default('结束对局').description('主动结束对局(中止当前频道对局)'),
48
+ commandView: koishi_1.Schema.string().default('查看').description('查看当前棋盘'),
49
+ commandStats: koishi_1.Schema.string().default('我的战绩').description('查看个人三棋战绩'),
50
+ goKomi: koishi_1.Schema.number().default(7.5).min(0).max(20).step(0.5).description('围棋贴目'),
51
+ });
52
+ function apply(ctx, config) {
53
+ // 数据库模型:玩家战绩
54
+ ctx.model.extend('chess_player_stats', {
55
+ id: 'unsigned',
56
+ userId: 'string',
57
+ platform: 'string',
58
+ goGames: 'unsigned',
59
+ goWins: 'unsigned',
60
+ goLosses: 'unsigned',
61
+ goDraws: 'unsigned',
62
+ gomokuGames: 'unsigned',
63
+ gomokuWins: 'unsigned',
64
+ gomokuLosses: 'unsigned',
65
+ gomokuDraws: 'unsigned',
66
+ othelloGames: 'unsigned',
67
+ othelloWins: 'unsigned',
68
+ othelloLosses: 'unsigned',
69
+ othelloDraws: 'unsigned',
70
+ }, { autoInc: true });
71
+ // 延迟导入,避免循环依赖
72
+ let manager;
73
+ ctx.on('ready', async () => {
74
+ const { GameManager } = await Promise.resolve().then(() => __importStar(require('./manager')));
75
+ manager = new GameManager(ctx, config);
76
+ });
77
+ ctx.command(`${config.commandStart} <game> [...options]`)
78
+ .action(async ({ session }, game, ...options) => {
79
+ if (!session)
80
+ return '无法获取会话信息。';
81
+ if (game === '帮助' || game === 'help') {
82
+ return manager.showHelp();
83
+ }
84
+ return manager.startGame(session, game, options.join(' '));
85
+ });
86
+ ctx.command(`${config.commandPlace} <position>`)
87
+ .action(async ({ session }, position) => {
88
+ if (!session)
89
+ return '无法获取会话信息。';
90
+ if (!position)
91
+ return `请输入落子坐标,例如:${config.commandPlace} A1`;
92
+ return manager.placeStone(session, position);
93
+ });
94
+ ctx.command(config.commandPass)
95
+ .action(async ({ session }) => {
96
+ if (!session)
97
+ return '无法获取会话信息。';
98
+ return manager.pass(session);
99
+ });
100
+ ctx.command(config.commandUndo)
101
+ .action(async ({ session }) => {
102
+ if (!session)
103
+ return '无法获取会话信息。';
104
+ return manager.undo(session);
105
+ });
106
+ ctx.command(config.commandResign)
107
+ .action(async ({ session }) => {
108
+ if (!session)
109
+ return '无法获取会话信息。';
110
+ return manager.resign(session);
111
+ });
112
+ ctx.command(config.commandEnd)
113
+ .action(async ({ session }) => {
114
+ if (!session)
115
+ return '无法获取会话信息。';
116
+ return manager.endGame(session);
117
+ });
118
+ ctx.command(config.commandView)
119
+ .action(async ({ session }) => {
120
+ if (!session)
121
+ return '无法获取会话信息。';
122
+ return manager.viewBoard(session);
123
+ });
124
+ ctx.command(config.commandStats)
125
+ .action(async ({ session }) => {
126
+ if (!session)
127
+ return '无法获取会话信息。';
128
+ return manager.showStats(session);
129
+ });
130
+ // ── 各棋类帮助指令 ──────────────────────────────────────────────
131
+ const gameHelpCommands = [
132
+ { name: '围棋', type: 'go', desc: '围棋帮助' },
133
+ { name: '五子棋', type: 'gomoku', desc: '五子棋帮助' },
134
+ { name: '黑白棋', type: 'othello', desc: '黑白棋帮助' },
135
+ ];
136
+ for (const { name, type } of gameHelpCommands) {
137
+ ctx.command(`${name} [...args]`)
138
+ .action(async ({ session }, ...args) => {
139
+ if (!session)
140
+ return '无法获取会话信息。';
141
+ const arg = args.join(' ').trim();
142
+ if (arg === '帮助' || arg === 'help' || !arg) {
143
+ return manager.showGameHelp(type);
144
+ }
145
+ return `未知参数「${arg}」。输入「${name} 帮助」查看详细规则,或使用「${config.commandStart} ${name}」创建对局。`;
146
+ });
147
+ }
148
+ }