koishi-plugin-cchess 1.1.0 → 1.2.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.
- package/README.md +7 -11
- package/lib/index.d.ts +1 -1
- package/lib/index.js +109 -240
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -8,25 +8,21 @@
|
|
|
8
8
|
yarn add koishi-plugin-cchess
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
在 Koishi 配置中启用,并提供 database
|
|
11
|
+
在 Koishi 配置中启用,并提供 database 与 canvas 服务。
|
|
12
12
|
|
|
13
13
|
## 指令
|
|
14
14
|
|
|
15
15
|
| 指令 | 说明 |
|
|
16
16
|
| --- | --- |
|
|
17
|
-
| `cchess
|
|
18
|
-
| `cchess
|
|
19
|
-
| `cchess
|
|
20
|
-
| `cchess.移动 <着法>` | 落子 |
|
|
21
|
-
| `cchess.悔棋` | 请求悔棋 |
|
|
22
|
-
| `cchess.悔棋 <同意/拒绝>` | 表决悔棋 |
|
|
17
|
+
| `cchess.开始 [红/黑] [人人/人机]` | 入座并开局,双方就位后自动开战 |
|
|
18
|
+
| `cchess.开始 <FEN>` | 摆谱,导入局面后再 `cchess.开始` 即可 |
|
|
19
|
+
| `cchess.悔棋 [同意/拒绝]` | 请求悔棋,也可直接回复同意或拒绝表决 |
|
|
23
20
|
| `cchess.认输` | 认输 |
|
|
24
21
|
| `cchess.结束` | 强制结束棋局 |
|
|
25
|
-
| `cchess
|
|
26
|
-
| `cchess.编辑棋盘.导出` | 导出局面 |
|
|
22
|
+
| `cchess.棋绩 [@某人/榜] [胜场/输场] [人数]` | 查询战绩与排行榜 |
|
|
27
23
|
| `cchess.查看云库残局 [DTM/DTC]` | 云库残局统计 |
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
|
|
25
|
+
发送 `cchess.开始` 即入座,双方就位后自动开局,无需单独的加入与退出指令。
|
|
30
26
|
|
|
31
27
|
着法可直接发送,例如 `炮二平五` 或 `b2e2`。
|
|
32
28
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
2
|
export declare const name = "cchess";
|
|
3
|
-
export declare const usage = "## \u4F7F\u7528\n\n`cchess.\
|
|
3
|
+
export declare const usage = "## \u4F7F\u7528\n\n`cchess.\u5F00\u59CB` \u5373\u53EF\u5165\u5EA7\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u5C40\uFF1B`cchess.\u5F00\u59CB \u4EBA\u673A` \u76F4\u63A5\u6311\u6218\u76AE\u5361\u9C7C\u3002\u7740\u6CD5\u53EF\u76F4\u63A5\u53D1\u9001\uFF0C\u4F8B\u5982 `\u70AE\u4E8C\u5E73\u4E94` \u6216 `b2e2`\u3002\n\n\u6094\u68CB\u8BF7\u6C42\u9001\u51FA\u540E\uFF0C\u5BF9\u65B9\u65E0\u9700\u5E26\u6307\u4EE4\u524D\u7F00\uFF0C\u76F4\u63A5\u56DE\u590D `\u540C\u610F` \u6216 `\u62D2\u7EDD` \u5373\u53EF\u8868\u51B3\u3002\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `cchess.\u5F00\u59CB [\u7EA2/\u9ED1] [\u4EBA\u4EBA/\u4EBA\u673A]` | \u5165\u5EA7\u5E76\u5F00\u5C40\uFF0C\u53CC\u65B9\u5C31\u4F4D\u540E\u81EA\u52A8\u5F00\u6218 |\n| `cchess.\u5F00\u59CB <FEN>` | \u6446\u8C31\uFF0C\u5BFC\u5165\u5C40\u9762\u540E\u518D `cchess.\u5F00\u59CB` \u5373\u53EF |\n| `cchess.\u6094\u68CB [\u540C\u610F/\u62D2\u7EDD]` | \u8BF7\u6C42\u6094\u68CB\uFF0C\u4E5F\u53EF\u76F4\u63A5\u56DE\u590D\u540C\u610F\u6216\u62D2\u7EDD\u8868\u51B3 |\n| `cchess.\u8BA4\u8F93` | \u8BA4\u8F93 |\n| `cchess.\u7ED3\u675F` | \u5F3A\u5236\u7ED3\u675F\u68CB\u5C40 |\n| `cchess.\u68CB\u7EE9 [@\u67D0\u4EBA/\u699C] [\u80DC\u573A/\u8F93\u573A] [\u4EBA\u6570]` | \u67E5\u8BE2\u6218\u7EE9\u4E0E\u6392\u884C\u699C |\n| `cchess.\u67E5\u770B\u4E91\u5E93\u6B8B\u5C40 [DTM/DTC]` | \u4E91\u5E93\u6B8B\u5C40\u7EDF\u8BA1 |";
|
|
4
4
|
export declare const inject: string[];
|
|
5
5
|
export interface Config {
|
|
6
6
|
boardSkin: string;
|
package/lib/index.js
CHANGED
|
@@ -43,7 +43,7 @@ var fs = __toESM(require("fs"));
|
|
|
43
43
|
var name = "cchess";
|
|
44
44
|
var usage = `## 使用
|
|
45
45
|
|
|
46
|
-
\`cchess
|
|
46
|
+
\`cchess.开始\` 即可入座,双方就位后自动开局;\`cchess.开始 人机\` 直接挑战皮卡鱼。着法可直接发送,例如 \`炮二平五\` 或 \`b2e2\`。
|
|
47
47
|
|
|
48
48
|
悔棋请求送出后,对方无需带指令前缀,直接回复 \`同意\` 或 \`拒绝\` 即可表决。
|
|
49
49
|
|
|
@@ -51,26 +51,20 @@ var usage = `## 使用
|
|
|
51
51
|
|
|
52
52
|
| 指令 | 说明 |
|
|
53
53
|
| --- | --- |
|
|
54
|
-
| \`cchess
|
|
55
|
-
| \`cchess
|
|
56
|
-
| \`cchess
|
|
57
|
-
| \`cchess.移动 <着法>\` | 落子 |
|
|
58
|
-
| \`cchess.悔棋\` | 请求悔棋 |
|
|
59
|
-
| \`cchess.悔棋 <同意/拒绝>\` | 表决悔棋,也可直接回复同意或拒绝 |
|
|
54
|
+
| \`cchess.开始 [红/黑] [人人/人机]\` | 入座并开局,双方就位后自动开战 |
|
|
55
|
+
| \`cchess.开始 <FEN>\` | 摆谱,导入局面后再 \`cchess.开始\` 即可 |
|
|
56
|
+
| \`cchess.悔棋 [同意/拒绝]\` | 请求悔棋,也可直接回复同意或拒绝表决 |
|
|
60
57
|
| \`cchess.认输\` | 认输 |
|
|
61
58
|
| \`cchess.结束\` | 强制结束棋局 |
|
|
62
|
-
| \`cchess
|
|
63
|
-
| \`cchess
|
|
64
|
-
|
|
65
|
-
| \`cchess.排行榜 [胜场/输场] [人数]\` | 排行榜 |
|
|
66
|
-
| \`cchess.查询玩家记录 [@某人]\` | 战绩 |`;
|
|
67
|
-
var inject = ["database", "puppeteer", "canvas"];
|
|
59
|
+
| \`cchess.棋绩 [@某人/榜] [胜场/输场] [人数]\` | 查询战绩与排行榜 |
|
|
60
|
+
| \`cchess.查看云库残局 [DTM/DTC]\` | 云库残局统计 |`;
|
|
61
|
+
var inject = ["database", "canvas"];
|
|
68
62
|
var pieceSkins = ["棋弈无限图形棋子", "云库木制棋子", "刘炳森红黑牛角隶书棋子", "木制棋子", "本纹隶书棋子", "银灰将军体棋子", "棋弈无限红绿正棋子", "棋弈无限红绿棋子", "棋天大圣棋子", "棋者象棋棋子", "楷书玉石棋子", "水墨青瓷棋子", "牛皮纸华康金文棋子", "皮卡鱼棋子", "红蓝祥隶棋子", "红黑精典无阴影棋子", "红黑精典棋子", "行书玉石棋子", "象甲棋子", "金属棋子篆体棋子", "镌刻华彩棋子", "鹏飞红黑棋子", "鹏飞经典棋子", "鹏飞绿龙棋子", "龙腾四海棋子"];
|
|
69
63
|
var boardSkins = ["棋弈无限红绿棋盘", "一鸣惊人棋盘", "三星堆棋盘", "云库木制棋盘", "云心鹤眼棋盘", "兰亭集序无字棋盘", "兰亭集序棋盘", "凤舞九天棋盘", "卯兔添福棋盘", "叱咤风云棋盘", "君临天下棋盘", "壁画古梦棋盘", "大闹天宫棋盘", "天山共色棋盘", "女神之约棋盘", "小吕飞刀棋盘", "山海绘卷棋盘", "护眼绿棋盘", "星河璀璨棋盘", "木制棋盘", "本纹隶书白棋盘", "本纹隶书黑棋盘", "桃花源记棋盘", "棋天大圣棋盘", "棋天无大圣棋盘", "武侠江湖棋盘", "水墨青瓷棋盘", "清悠茶道棋盘", "游园惊梦棋盘", "牛转乾坤棋盘", "玉石太极棋盘", "皓月无痕棋盘", "皮卡鱼棋盘", "盲棋神迹棋盘", "空城计棋盘", "竹波烟雨棋盘", "经典木棋盘棋盘", "绿色棋盘", "象甲2023棋盘", "象甲征程棋盘", "象甲重燃棋盘", "金牛降世棋盘", "金虎贺岁棋盘", "金虎贺岁(王天一签名版)棋盘", "鎏金岁月棋盘", "霸王别姬棋盘", "鸿门宴棋盘", "鹏飞红黑棋盘", "鹏飞经典棋盘", "鹏飞绿龙棋盘", "龙腾天坛棋盘"];
|
|
70
64
|
var Config = import_koishi.Schema.object({
|
|
71
65
|
boardSkin: import_koishi.Schema.union(boardSkins).default("象甲2023棋盘").description(`棋盘皮肤。`),
|
|
72
66
|
pieceSkin: import_koishi.Schema.union(pieceSkins).default("象甲棋子").description(`棋子皮肤。`),
|
|
73
|
-
allowFreePieceMovementInHumanMachineMode: import_koishi.Schema.boolean().default(false).description(
|
|
67
|
+
allowFreePieceMovementInHumanMachineMode: import_koishi.Schema.boolean().default(false).description(`是否允许在人机模式下所有用户都可以自由移动棋子,开启后可以不入座直接开始人机对局。`),
|
|
74
68
|
defaultEngineThinkingDepth: import_koishi.Schema.number().min(0).max(100).default(10).description(`默认引擎思考深度,越高 AI 棋力越强,耗时也越长(小于 1 时按 1 计算)。由于 Nodejs 不支持 SIMD,所以不建议设置过高。`),
|
|
75
69
|
defaultMaxLeaderboardEntries: import_koishi.Schema.number().min(0).default(10).description(`显示排行榜时默认的最大人数。`),
|
|
76
70
|
retractDelay: import_koishi.Schema.number().min(0).default(0).description(`自动撤回等待的时间,单位是秒。值为 0 时不启用自动撤回功能。`),
|
|
@@ -238,131 +232,82 @@ function apply(ctx, config) {
|
|
|
238
232
|
if (playerRecord.length !== 0 && playerRecord[0].side !== convertTurnToString(gameRecord.turn)) {
|
|
239
233
|
return next();
|
|
240
234
|
}
|
|
241
|
-
await session
|
|
235
|
+
await handleMove(session, moveOperation);
|
|
242
236
|
});
|
|
243
237
|
ctx.command("cchess", "中国象棋游戏指令帮助").action(async ({ session }) => {
|
|
244
238
|
await session.execute(`cchess -h`);
|
|
245
239
|
});
|
|
246
|
-
ctx.command("cchess
|
|
240
|
+
ctx.command("cchess.开始 [args:text]", "入座并开局;可选「红 / 黑」「人人 / 人机」,也可直接粘贴 FEN 摆谱").action(async ({ session }, args) => {
|
|
247
241
|
const { username, userId, channelId } = session;
|
|
248
242
|
await updateNameInPlayerRecord(userId, username);
|
|
249
243
|
const gameRecord = await getGameRecord(channelId);
|
|
250
244
|
if (gameRecord.isStarted) {
|
|
245
|
+
return await sendMessage(session, alreadyStartedPanel(username));
|
|
246
|
+
}
|
|
247
|
+
const text = (args ?? "").trim();
|
|
248
|
+
if (text && isValidateFen(text)) {
|
|
249
|
+
const startFen = text.includes("moves") ? text.split("moves")[0].trim() : text;
|
|
250
|
+
await ctx.database.set("cchess_game_records", { channelId }, {
|
|
251
|
+
turn: fenToTurn(text),
|
|
252
|
+
board: fenToBoard(text),
|
|
253
|
+
moveList: [],
|
|
254
|
+
movesAfterLastEat: [],
|
|
255
|
+
boardAfterLastEat: fenToBoard(text),
|
|
256
|
+
turnAfterLastEat: fenToTurn(text),
|
|
257
|
+
lastMove: null,
|
|
258
|
+
isHistoryMode: false,
|
|
259
|
+
originalState: {},
|
|
260
|
+
startFen
|
|
261
|
+
});
|
|
262
|
+
await parseFen(channelId, text);
|
|
263
|
+
const record = await getGameRecord(channelId);
|
|
251
264
|
return await sendMessage(session, panel({
|
|
252
|
-
icon: "
|
|
253
|
-
title: "
|
|
265
|
+
icon: "✅",
|
|
266
|
+
title: "棋盘摆放完毕",
|
|
254
267
|
at: username,
|
|
255
|
-
body: ["
|
|
256
|
-
tips: ["
|
|
268
|
+
body: [field("轮走方", withSideIcon(convertTurnToString(record.turn)))],
|
|
269
|
+
tips: ["发送「cchess.开始」由此局面开战"],
|
|
257
270
|
image: await renderBoard(channelId)
|
|
258
271
|
}));
|
|
259
272
|
}
|
|
260
|
-
|
|
261
|
-
if (choice?.includes("红") && choice.includes("黑")) {
|
|
273
|
+
if (text.includes("红") && text.includes("黑")) {
|
|
262
274
|
return await sendMessage(session, panel({
|
|
263
275
|
icon: "⚠️",
|
|
264
276
|
title: "一次只能选一方",
|
|
265
277
|
at: username,
|
|
266
278
|
body: ["请只选择红方或黑方。"],
|
|
267
|
-
tips: ["「cchess
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
if (choice?.includes("红")) choice = "红方";
|
|
271
|
-
else if (choice?.includes("黑")) choice = "黑方";
|
|
272
|
-
else choice = Math.random() < 0.5 ? "红方" : "黑方";
|
|
273
|
-
const selfRecords = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
274
|
-
const allRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
275
|
-
const playersNum = allRecords.length;
|
|
276
|
-
if (selfRecords.length === 0) {
|
|
277
|
-
await ctx.database.create("cchess_gaming_player_records", { channelId, userId, username, side: choice });
|
|
278
|
-
return await sendMessage(session, panel({
|
|
279
|
-
icon: "✅",
|
|
280
|
-
title: "入座成功",
|
|
281
|
-
at: username,
|
|
282
|
-
body: [field("阵营", withSideIcon(choice)), field("当前人数", `${playersNum + 1} 人`)],
|
|
283
|
-
tips: ["再次发送「cchess.加入 红 / 黑」即可换边", "人齐后发送「cchess.开始 人人」开局"]
|
|
284
|
-
}));
|
|
285
|
-
} else {
|
|
286
|
-
await ctx.database.set("cchess_gaming_player_records", { channelId, userId }, { side: choice });
|
|
287
|
-
return await sendMessage(session, panel({
|
|
288
|
-
icon: "✅",
|
|
289
|
-
title: "换边成功",
|
|
290
|
-
at: username,
|
|
291
|
-
body: [field("新的阵营", withSideIcon(choice)), field("当前人数", `${playersNum} 人`)]
|
|
292
|
-
}));
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
ctx.command("cchess.退出", "退出游戏").action(async ({ session }) => {
|
|
296
|
-
const { username, userId, channelId } = session;
|
|
297
|
-
await updateNameInPlayerRecord(userId, username);
|
|
298
|
-
const gameRecord = await getGameRecord(channelId);
|
|
299
|
-
if (gameRecord.isStarted) {
|
|
300
|
-
return await sendMessage(session, panel({
|
|
301
|
-
icon: "⚠️",
|
|
302
|
-
title: "棋局已经开战",
|
|
303
|
-
at: username,
|
|
304
|
-
body: ["对局进行中,无法退出。"],
|
|
305
|
-
tips: ["可发送「cchess.认输」结束本局"]
|
|
279
|
+
tips: ["「cchess.开始 红」执红先行", "「cchess.开始 黑」执黑应招"]
|
|
306
280
|
}));
|
|
307
281
|
}
|
|
308
|
-
const
|
|
309
|
-
const allRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
310
|
-
const playersNum = allRecords.length;
|
|
311
|
-
if (selfRecords.length === 0) {
|
|
312
|
-
return await sendMessage(session, notJoinedPanel(username));
|
|
313
|
-
} else {
|
|
314
|
-
await ctx.database.remove("cchess_gaming_player_records", { channelId, userId });
|
|
315
|
-
return await sendMessage(session, panel({
|
|
316
|
-
icon: "✅",
|
|
317
|
-
title: "已离席",
|
|
318
|
-
at: username,
|
|
319
|
-
body: ["已退出本局。", field("剩余人数", `${playersNum - 1} 人`)]
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
ctx.command("cchess.开始 [mode:string]", "开始对局,mode 可选「人人」或「人机」").action(async ({ session }, mode) => {
|
|
324
|
-
const { username, userId, channelId } = session;
|
|
325
|
-
await updateNameInPlayerRecord(userId, username);
|
|
326
|
-
const gameRecord = await getGameRecord(channelId);
|
|
327
|
-
if (gameRecord.isStarted) {
|
|
328
|
-
return await sendMessage(session, alreadyStartedPanel(username));
|
|
329
|
-
}
|
|
330
|
-
const playerRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
331
|
-
const playersNum = playerRecords.length;
|
|
332
|
-
if (/机|电脑|ai|engine|pve/i.test(mode ?? "")) {
|
|
333
|
-
return await startVersusEngine(session, gameRecord, playerRecords);
|
|
334
|
-
}
|
|
282
|
+
const chosenSide = text.includes("红") ? "红方" : text.includes("黑") ? "黑方" : "";
|
|
335
283
|
void checkEngine(channelId);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
284
|
+
if (/机|电脑|ai|engine|pve/i.test(text)) {
|
|
285
|
+
const players2 = await seatPlayer(channelId, userId, username, chosenSide);
|
|
286
|
+
return await startVersusEngine(session, gameRecord, players2);
|
|
287
|
+
}
|
|
288
|
+
const players = await seatPlayer(channelId, userId, username, chosenSide);
|
|
289
|
+
const redPlayers = players.filter((player) => player.side === "红方");
|
|
290
|
+
const blackPlayers = players.filter((player) => player.side === "黑方");
|
|
291
|
+
const selfSide = players.find((player) => player.userId === userId)?.side ?? "";
|
|
292
|
+
if (!redPlayers.length || !blackPlayers.length) {
|
|
340
293
|
return await sendMessage(session, panel({
|
|
341
|
-
icon: "
|
|
342
|
-
title: "
|
|
294
|
+
icon: "⏳",
|
|
295
|
+
title: "已入座,静候对手",
|
|
343
296
|
at: username,
|
|
344
|
-
body: [
|
|
345
|
-
|
|
297
|
+
body: [
|
|
298
|
+
field("阵营", withSideIcon(selfSide)),
|
|
299
|
+
field("当前人数", `${players.length} 人`),
|
|
300
|
+
!blackPlayers.length ? "黑方尚无人。" : "红方尚无人。"
|
|
301
|
+
],
|
|
302
|
+
tips: ["好友发送「cchess.开始」补位,双方就位即自动开局", "「cchess.开始 黑」可指定阵营"]
|
|
346
303
|
}));
|
|
347
304
|
}
|
|
348
|
-
if (redPlayers.length !== 1 || blackPlayers.length !== 1) {
|
|
349
|
-
const randomPlayer = playerRecords[Math.floor(Math.random() * playerRecords.length)];
|
|
350
|
-
randomPlayer.side = redPlayers.length < 1 ? "红方" : "黑方";
|
|
351
|
-
assignNotice = `🎲 @${randomPlayer.username} 被随机分配至 ${withSideIcon(randomPlayer.side)}`;
|
|
352
|
-
await ctx.database.set("cchess_gaming_player_records", {
|
|
353
|
-
channelId,
|
|
354
|
-
userId: randomPlayer.userId
|
|
355
|
-
}, { side: randomPlayer.side });
|
|
356
|
-
redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
357
|
-
blackPlayers = playerRecords.filter((player) => player.side === "黑方");
|
|
358
|
-
}
|
|
359
305
|
await ctx.database.set("cchess_game_records", { channelId }, { isStarted: true });
|
|
360
306
|
const sideString = convertTurnToString(gameRecord.turn);
|
|
361
307
|
return await sendMessage(session, panel({
|
|
362
308
|
icon: "✅",
|
|
363
309
|
title: "楚河汉界,对局开始",
|
|
364
310
|
body: [
|
|
365
|
-
...assignNotice ? [assignNotice, ""] : [],
|
|
366
311
|
`${withSideIcon("红方")}(${redPlayers.length})`,
|
|
367
312
|
...redPlayers.map((player) => ` @${player.username}`),
|
|
368
313
|
"",
|
|
@@ -385,7 +330,7 @@ function apply(ctx, config) {
|
|
|
385
330
|
title: "棋盘尚且空空",
|
|
386
331
|
at: username,
|
|
387
332
|
body: ["当前没有进行中的对局,无需收拾残局。"],
|
|
388
|
-
tips: ["发送「cchess
|
|
333
|
+
tips: ["发送「cchess.开始」入座,再开一局"]
|
|
389
334
|
}));
|
|
390
335
|
}
|
|
391
336
|
await endGame(channelId);
|
|
@@ -394,10 +339,10 @@ function apply(ctx, config) {
|
|
|
394
339
|
title: "对局已强制结束",
|
|
395
340
|
at: username,
|
|
396
341
|
body: ["棋盘已收,胜负不计。"],
|
|
397
|
-
tips: ["发送「cchess
|
|
342
|
+
tips: ["发送「cchess.开始」重整旗鼓"]
|
|
398
343
|
}));
|
|
399
344
|
});
|
|
400
|
-
|
|
345
|
+
async function handleMove(session, moveOperation) {
|
|
401
346
|
const { username, userId, channelId } = session;
|
|
402
347
|
await updateNameInPlayerRecord(userId, username);
|
|
403
348
|
moveOperation = moveOperation?.trim();
|
|
@@ -519,7 +464,8 @@ function apply(ctx, config) {
|
|
|
519
464
|
} finally {
|
|
520
465
|
busyChannels.delete(channelId);
|
|
521
466
|
}
|
|
522
|
-
}
|
|
467
|
+
}
|
|
468
|
+
__name(handleMove, "handleMove");
|
|
523
469
|
ctx.command("cchess.悔棋 [decision:string]", "请求悔棋;待决时由对方以「同意 / 拒绝」表决").action(async ({ session }, decision) => {
|
|
524
470
|
const { username, userId, channelId } = session;
|
|
525
471
|
await updateNameInPlayerRecord(userId, username);
|
|
@@ -635,109 +581,30 @@ function apply(ctx, config) {
|
|
|
635
581
|
body: isDtc ? ["DTC:距吃子的步数统计。", "https://www.chessdb.cn/egtb_info.html"] : ["DTM:距将死的步数统计。", "https://www.chessdb.cn/egtb_info_dtm.html"]
|
|
636
582
|
}));
|
|
637
583
|
});
|
|
638
|
-
ctx.command("cchess
|
|
639
|
-
const { username, userId, channelId } = session;
|
|
640
|
-
await updateNameInPlayerRecord(userId, username);
|
|
641
|
-
if (!fen) {
|
|
642
|
-
return await sendMessage(session, panel({
|
|
643
|
-
icon: "📋",
|
|
644
|
-
title: "FEN 串使用方法",
|
|
645
|
-
at: username,
|
|
646
|
-
body: ["中国象棋 FEN 格式规范:", "https://www.xqbase.com/protocol/cchess_fen.htm"]
|
|
647
|
-
}));
|
|
648
|
-
}
|
|
649
|
-
const gameRecord = await getGameRecord(channelId);
|
|
650
|
-
if (gameRecord.isStarted) {
|
|
651
|
-
return await sendMessage(session, panel({
|
|
652
|
-
icon: "⚠️",
|
|
653
|
-
title: "棋局进行中",
|
|
654
|
-
at: username,
|
|
655
|
-
body: ["对弈期间不可改动棋盘。"],
|
|
656
|
-
tips: ["发送「cchess.结束」收局后再行摆谱"]
|
|
657
|
-
}));
|
|
658
|
-
}
|
|
659
|
-
fen = fen?.trim();
|
|
660
|
-
if (!fen || !isValidateFen(fen)) {
|
|
661
|
-
return await sendMessage(session, panel({
|
|
662
|
-
icon: "❌",
|
|
663
|
-
title: "FEN 串无法解析",
|
|
664
|
-
at: username,
|
|
665
|
-
body: ["请检查局面串的格式是否完整。"],
|
|
666
|
-
tips: [
|
|
667
|
-
"示例:rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w",
|
|
668
|
-
"发送「cchess.编辑棋盘」查看规范"
|
|
669
|
-
]
|
|
670
|
-
}));
|
|
671
|
-
}
|
|
672
|
-
const newStartFen = fen.includes("moves") ? fen.split("moves")[0].trim() : fen;
|
|
673
|
-
await ctx.database.set("cchess_game_records", { channelId }, {
|
|
674
|
-
turn: fenToTurn(fen),
|
|
675
|
-
board: fenToBoard(fen),
|
|
676
|
-
moveList: [],
|
|
677
|
-
movesAfterLastEat: [],
|
|
678
|
-
boardAfterLastEat: fenToBoard(fen),
|
|
679
|
-
turnAfterLastEat: fenToTurn(fen),
|
|
680
|
-
lastMove: null,
|
|
681
|
-
isHistoryMode: false,
|
|
682
|
-
originalState: {},
|
|
683
|
-
startFen: newStartFen
|
|
684
|
-
});
|
|
685
|
-
await parseFen(channelId, fen);
|
|
686
|
-
const record = await getGameRecord(channelId);
|
|
687
|
-
return await sendMessage(session, panel({
|
|
688
|
-
icon: "✅",
|
|
689
|
-
title: "棋盘摆放完毕",
|
|
690
|
-
at: username,
|
|
691
|
-
body: [field("轮走方", withSideIcon(convertTurnToString(record.turn)))],
|
|
692
|
-
tips: ["发送「cchess.开始 人人」或「cchess.开始 人机」由此局面开战"],
|
|
693
|
-
image: await renderBoard(channelId)
|
|
694
|
-
}));
|
|
695
|
-
});
|
|
696
|
-
ctx.command("cchess.编辑棋盘.导出", "导出FEN串").action(async ({ session }) => {
|
|
697
|
-
const { username, userId, channelId } = session;
|
|
698
|
-
await updateNameInPlayerRecord(userId, username);
|
|
699
|
-
const gameRecord = await getGameRecord(channelId);
|
|
700
|
-
if (!gameRecord.isStarted) {
|
|
701
|
-
return await sendMessage(session, notStartedPanel(username));
|
|
702
|
-
}
|
|
703
|
-
if (gameRecord.isAnalyzing || busyChannels.has(channelId)) {
|
|
704
|
-
return await sendMessage(session, analyzingPanel(username));
|
|
705
|
-
}
|
|
706
|
-
const fenWithFullMove = await getFenWithFullMove(channelId);
|
|
707
|
-
return await sendMessage(session, panel({
|
|
708
|
-
icon: "📋",
|
|
709
|
-
title: "当前局面 FEN",
|
|
710
|
-
at: username,
|
|
711
|
-
body: [fenWithFullMove],
|
|
712
|
-
tips: ["可用「cchess.编辑棋盘 <FEN>」还原此局面"]
|
|
713
|
-
}));
|
|
714
|
-
});
|
|
715
|
-
ctx.command("cchess.排行榜 [type:string] [number:number]", "查看玩家排行榜").action(async ({ session }, type, number = config.defaultMaxLeaderboardEntries) => {
|
|
716
|
-
const { userId, username } = session;
|
|
717
|
-
await updateNameInPlayerRecord(userId, username);
|
|
718
|
-
if (type && /^\d+$/.test(type)) {
|
|
719
|
-
number = Number(type);
|
|
720
|
-
type = "";
|
|
721
|
-
}
|
|
722
|
-
if (typeof number !== "number" || isNaN(number) || number < 0) {
|
|
723
|
-
return await sendMessage(session, invalidLeaderboardSizePanel(username));
|
|
724
|
-
}
|
|
725
|
-
const byLose = !!type && /负|输|lose/i.test(type);
|
|
726
|
-
return await getLeaderboard(
|
|
727
|
-
session,
|
|
728
|
-
byLose ? "lose" : "win",
|
|
729
|
-
byLose ? "总输场排行榜" : "总胜场排行榜",
|
|
730
|
-
byLose ? "🍂" : "🏆",
|
|
731
|
-
number
|
|
732
|
-
);
|
|
733
|
-
});
|
|
734
|
-
ctx.command("cchess.查询玩家记录 [targetUser:text]", "查询玩家记录").action(async ({ session }, targetUser) => {
|
|
584
|
+
ctx.command("cchess.棋绩 [args:text]", "查询战绩;带「榜」看排行榜,@某人 可查看对方战绩").action(async ({ session }, args) => {
|
|
735
585
|
let { userId, username } = session;
|
|
736
586
|
await updateNameInPlayerRecord(userId, username);
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
587
|
+
const text = (args ?? "").trim();
|
|
588
|
+
const byLose = /负|输|lose/i.test(text);
|
|
589
|
+
const numberMatch = text.match(/\d+/);
|
|
590
|
+
const hasTarget = text.includes("<at");
|
|
591
|
+
const isLeaderboard = !hasTarget && (/榜|排行|排名|leaderboard/i.test(text) || byLose || !!numberMatch);
|
|
592
|
+
if (isLeaderboard) {
|
|
593
|
+
const size = numberMatch ? Number(numberMatch[0]) : config.defaultMaxLeaderboardEntries;
|
|
594
|
+
if (isNaN(size) || size < 0) {
|
|
595
|
+
return await sendMessage(session, invalidLeaderboardSizePanel(username));
|
|
596
|
+
}
|
|
597
|
+
return await getLeaderboard(
|
|
598
|
+
session,
|
|
599
|
+
byLose ? "lose" : "win",
|
|
600
|
+
byLose ? "总输场排行榜" : "总胜场排行榜",
|
|
601
|
+
byLose ? "🍂" : "🏆",
|
|
602
|
+
size
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
if (hasTarget) {
|
|
606
|
+
const replaced = await replaceAtTags(session, text);
|
|
607
|
+
const match = replaced.match(/<at id="([^"]+)"(?: name="([^"]+)")?\/>/);
|
|
741
608
|
userId = match?.[1] ?? userId;
|
|
742
609
|
username = match?.[2] ?? username;
|
|
743
610
|
}
|
|
@@ -754,7 +621,7 @@ function apply(ctx, config) {
|
|
|
754
621
|
title: "棋士档案",
|
|
755
622
|
at: session.username,
|
|
756
623
|
body: [field("查询对象", username), "尚无对局记录,静待首战。"],
|
|
757
|
-
tips: ["发送「cchess
|
|
624
|
+
tips: ["发送「cchess.开始」开启第一局"]
|
|
758
625
|
}));
|
|
759
626
|
}
|
|
760
627
|
const { win, lose } = targetUserRecord[0];
|
|
@@ -779,7 +646,7 @@ function apply(ctx, config) {
|
|
|
779
646
|
title: "棋局尚未开始",
|
|
780
647
|
at: username,
|
|
781
648
|
body: ["当前频道还没有进行中的对局。"],
|
|
782
|
-
tips: ["「cchess
|
|
649
|
+
tips: ["「cchess.开始」入座并对弈", "「cchess.开始 人机」挑战皮卡鱼"]
|
|
783
650
|
});
|
|
784
651
|
}
|
|
785
652
|
__name(notStartedPanel, "notStartedPanel");
|
|
@@ -798,8 +665,8 @@ function apply(ctx, config) {
|
|
|
798
665
|
icon: "⚠️",
|
|
799
666
|
title: "你尚未入座",
|
|
800
667
|
at: username,
|
|
801
|
-
body: ["
|
|
802
|
-
tips: ["发送「cchess
|
|
668
|
+
body: ["请先入座再落子。"],
|
|
669
|
+
tips: ["发送「cchess.开始」入座"]
|
|
803
670
|
});
|
|
804
671
|
}
|
|
805
672
|
__name(notJoinedPanel, "notJoinedPanel");
|
|
@@ -838,7 +705,7 @@ function apply(ctx, config) {
|
|
|
838
705
|
title: "皮卡鱼尚未就绪",
|
|
839
706
|
at: username,
|
|
840
707
|
body: ["引擎启动失败,暂时无法进行人机对战。"],
|
|
841
|
-
tips: ["稍后重试,或改用「cchess
|
|
708
|
+
tips: ["稍后重试,或改用「cchess.开始」人人对战"]
|
|
842
709
|
});
|
|
843
710
|
}
|
|
844
711
|
__name(engineUnavailablePanel, "engineUnavailablePanel");
|
|
@@ -848,7 +715,7 @@ function apply(ctx, config) {
|
|
|
848
715
|
title: "榜单人数有误",
|
|
849
716
|
at: username,
|
|
850
717
|
body: ["请输入不小于 0 的整数。"],
|
|
851
|
-
tips: ["例如:cchess
|
|
718
|
+
tips: ["例如:cchess.棋绩 榜 10"]
|
|
852
719
|
});
|
|
853
720
|
}
|
|
854
721
|
__name(invalidLeaderboardSizePanel, "invalidLeaderboardSizePanel");
|
|
@@ -874,7 +741,7 @@ function apply(ctx, config) {
|
|
|
874
741
|
field("胜方", withSideIcon(convertTurnToString(winSide))),
|
|
875
742
|
field("棋手", winners.length ? winners.map((player) => `@${player.username}`).join("、") : "皮卡鱼")
|
|
876
743
|
],
|
|
877
|
-
tips: ["发送「cchess
|
|
744
|
+
tips: ["发送「cchess.开始」再战一局"],
|
|
878
745
|
image
|
|
879
746
|
});
|
|
880
747
|
}
|
|
@@ -884,15 +751,6 @@ function apply(ctx, config) {
|
|
|
884
751
|
const playersNum = playerRecords.length;
|
|
885
752
|
const redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
886
753
|
const blackPlayers = playerRecords.filter((player) => player.side === "黑方");
|
|
887
|
-
if (playersNum < 1 && !config.allowFreePieceMovementInHumanMachineMode) {
|
|
888
|
-
return await sendMessage(session, panel({
|
|
889
|
-
icon: "⚠️",
|
|
890
|
-
title: "尚无棋手入座",
|
|
891
|
-
at: username,
|
|
892
|
-
body: ["人机对战至少需要 1 位棋手。"],
|
|
893
|
-
tips: ["发送「cchess.加入」即可入座挑战皮卡鱼"]
|
|
894
|
-
}));
|
|
895
|
-
}
|
|
896
754
|
if (!await checkEngine(channelId)) {
|
|
897
755
|
return await sendMessage(session, engineUnavailablePanel(username));
|
|
898
756
|
}
|
|
@@ -1027,7 +885,7 @@ function apply(ctx, config) {
|
|
|
1027
885
|
icon,
|
|
1028
886
|
title,
|
|
1029
887
|
body: topPlayers.length ? topPlayers.map((player, index) => `${MEDALS[index] ?? `${index + 1}.`} ${player.username} ${player[sortField]} 局`) : ["榜上尚无名姓,快去手谈一局。"],
|
|
1030
|
-
tips: topPlayers.length ? [] : ["发送「cchess
|
|
888
|
+
tips: topPlayers.length ? [] : ["发送「cchess.开始」开启对局"]
|
|
1031
889
|
}));
|
|
1032
890
|
}
|
|
1033
891
|
__name(getLeaderboard, "getLeaderboard");
|
|
@@ -1080,18 +938,29 @@ function apply(ctx, config) {
|
|
|
1080
938
|
return false;
|
|
1081
939
|
}
|
|
1082
940
|
__name(requestEngineMove, "requestEngineMove");
|
|
1083
|
-
async function
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
941
|
+
async function seatPlayer(channelId, userId, username, chosenSide) {
|
|
942
|
+
const players = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
943
|
+
const self = players.find((player) => player.userId === userId);
|
|
944
|
+
if (!self) {
|
|
945
|
+
await ctx.database.create("cchess_gaming_player_records", {
|
|
946
|
+
channelId,
|
|
947
|
+
userId,
|
|
948
|
+
username,
|
|
949
|
+
side: chosenSide || balancedSide(players)
|
|
950
|
+
});
|
|
951
|
+
} else if (chosenSide && self.side !== chosenSide) {
|
|
952
|
+
await ctx.database.set("cchess_gaming_player_records", { channelId, userId }, { side: chosenSide });
|
|
1091
953
|
}
|
|
1092
|
-
return
|
|
954
|
+
return await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
955
|
+
}
|
|
956
|
+
__name(seatPlayer, "seatPlayer");
|
|
957
|
+
function balancedSide(players) {
|
|
958
|
+
const redNum = players.filter((player) => player.side === "红方").length;
|
|
959
|
+
const blackNum = players.length - redNum;
|
|
960
|
+
if (redNum === blackNum) return Math.random() < 0.5 ? "红方" : "黑方";
|
|
961
|
+
return redNum < blackNum ? "红方" : "黑方";
|
|
1093
962
|
}
|
|
1094
|
-
__name(
|
|
963
|
+
__name(balancedSide, "balancedSide");
|
|
1095
964
|
function fenToTurn(t) {
|
|
1096
965
|
let e = t.split(" ");
|
|
1097
966
|
return "b" == e[1] ? "b" : "w";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cchess",
|
|
3
3
|
"description": "Koishi 的中国象棋插件。",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"service": {
|
|
17
17
|
"required": [
|
|
18
18
|
"database",
|
|
19
|
-
"puppeteer",
|
|
20
19
|
"canvas"
|
|
21
20
|
]
|
|
22
21
|
}
|
|
@@ -49,7 +48,6 @@
|
|
|
49
48
|
"koishi": "^4.18.11"
|
|
50
49
|
},
|
|
51
50
|
"devDependencies": {
|
|
52
|
-
"@koishijs/canvas": "^0.2.0"
|
|
53
|
-
"koishi-plugin-puppeteer": "^3.9.0"
|
|
51
|
+
"@koishijs/canvas": "^0.2.0"
|
|
54
52
|
}
|
|
55
53
|
}
|