koishi-plugin-cchess 1.0.7 → 1.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.
- package/README.md +37 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +220 -235
- package/package.json +3 -3
- package/readme.md +0 -69
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# koishi-plugin-cchess
|
|
2
|
+
|
|
3
|
+
中国象棋,支持人人对战与皮卡鱼人机对战
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yarn add koishi-plugin-cchess
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
在 Koishi 配置中启用,并提供 database、puppeteer 与 canvas 服务。
|
|
12
|
+
|
|
13
|
+
## 指令
|
|
14
|
+
|
|
15
|
+
| 指令 | 说明 |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `cchess.加入 [红/黑]` | 入座 |
|
|
18
|
+
| `cchess.退出` | 开局前离席 |
|
|
19
|
+
| `cchess.开始 [人人/人机]` | 开局,默认人人对战 |
|
|
20
|
+
| `cchess.移动 <着法>` | 落子 |
|
|
21
|
+
| `cchess.悔棋` | 请求悔棋 |
|
|
22
|
+
| `cchess.悔棋 <同意/拒绝>` | 表决悔棋 |
|
|
23
|
+
| `cchess.认输` | 认输 |
|
|
24
|
+
| `cchess.结束` | 强制结束棋局 |
|
|
25
|
+
| `cchess.编辑棋盘 <FEN>` | 导入局面 |
|
|
26
|
+
| `cchess.编辑棋盘.导出` | 导出局面 |
|
|
27
|
+
| `cchess.查看云库残局 [DTM/DTC]` | 云库残局统计 |
|
|
28
|
+
| `cchess.排行榜 [胜场/输场] [人数]` | 排行榜 |
|
|
29
|
+
| `cchess.查询玩家记录 [@某人]` | 战绩 |
|
|
30
|
+
|
|
31
|
+
着法可直接发送,例如 `炮二平五` 或 `b2e2`。
|
|
32
|
+
|
|
33
|
+
悔棋请求送出后,对方直接回复 `同意` 或 `拒绝` 即可表决,不必带指令前缀。
|
|
34
|
+
|
|
35
|
+
## 许可证
|
|
36
|
+
|
|
37
|
+
可按 [Apache-2.0](LICENSE-APACHE) 或 [MIT](LICENSE-MIT) 使用。
|
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\
|
|
3
|
+
export declare const usage = "## \u4F7F\u7528\n\n`cchess.\u52A0\u5165` \u5165\u5EA7\uFF0C`cchess.\u5F00\u59CB \u4EBA\u4EBA` \u6216 `cchess.\u5F00\u59CB \u4EBA\u673A` \u5F00\u5C40\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.\u52A0\u5165 [\u7EA2/\u9ED1]` | \u5165\u5EA7 |\n| `cchess.\u9000\u51FA` | \u5F00\u5C40\u524D\u79BB\u5E2D |\n| `cchess.\u5F00\u59CB [\u4EBA\u4EBA/\u4EBA\u673A]` | \u5F00\u5C40\uFF0C\u9ED8\u8BA4\u4EBA\u4EBA\u5BF9\u6218 |\n| `cchess.\u79FB\u52A8 <\u7740\u6CD5>` | \u843D\u5B50 |\n| `cchess.\u6094\u68CB` | \u8BF7\u6C42\u6094\u68CB |\n| `cchess.\u6094\u68CB <\u540C\u610F/\u62D2\u7EDD>` | \u8868\u51B3\u6094\u68CB\uFF0C\u4E5F\u53EF\u76F4\u63A5\u56DE\u590D\u540C\u610F\u6216\u62D2\u7EDD |\n| `cchess.\u8BA4\u8F93` | \u8BA4\u8F93 |\n| `cchess.\u7ED3\u675F` | \u5F3A\u5236\u7ED3\u675F\u68CB\u5C40 |\n| `cchess.\u7F16\u8F91\u68CB\u76D8 <FEN>` | \u5BFC\u5165\u5C40\u9762\uFF0C\u4E0D\u5E26\u53C2\u6570\u65F6\u67E5\u770B FEN \u7528\u6CD5 |\n| `cchess.\u7F16\u8F91\u68CB\u76D8.\u5BFC\u51FA` | \u5BFC\u51FA\u5C40\u9762 |\n| `cchess.\u67E5\u770B\u4E91\u5E93\u6B8B\u5C40 [DTM/DTC]` | \u4E91\u5E93\u6B8B\u5C40\u7EDF\u8BA1 |\n| `cchess.\u6392\u884C\u699C [\u80DC\u573A/\u8F93\u573A] [\u4EBA\u6570]` | \u6392\u884C\u699C |\n| `cchess.\u67E5\u8BE2\u73A9\u5BB6\u8BB0\u5F55 [@\u67D0\u4EBA]` | \u6218\u7EE9 |";
|
|
4
4
|
export declare const inject: string[];
|
|
5
5
|
export interface Config {
|
|
6
6
|
boardSkin: string;
|
package/lib/index.js
CHANGED
|
@@ -43,40 +43,27 @@ var fs = __toESM(require("fs"));
|
|
|
43
43
|
var name = "cchess";
|
|
44
44
|
var usage = `## 使用
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
\`cchess.加入\` 入座,\`cchess.开始 人人\` 或 \`cchess.开始 人机\` 开局。着法可直接发送,例如 \`炮二平五\` 或 \`b2e2\`。
|
|
47
|
+
|
|
48
|
+
悔棋请求送出后,对方无需带指令前缀,直接回复 \`同意\` 或 \`拒绝\` 即可表决。
|
|
48
49
|
|
|
49
50
|
## 指令
|
|
50
51
|
|
|
51
52
|
| 指令 | 说明 |
|
|
52
53
|
| --- | --- |
|
|
53
|
-
| \`cchess.加入 [红/黑]\` |
|
|
54
|
+
| \`cchess.加入 [红/黑]\` | 入座 |
|
|
54
55
|
| \`cchess.退出\` | 开局前离席 |
|
|
55
|
-
| \`cchess
|
|
56
|
-
| \`cchess
|
|
57
|
-
| \`cchess
|
|
58
|
-
| \`cchess
|
|
59
|
-
| \`cchess.认输\` |
|
|
60
|
-
| \`cchess.结束\` |
|
|
61
|
-
| \`cchess
|
|
62
|
-
| \`cchess.编辑棋盘.导出\` |
|
|
63
|
-
| \`cchess
|
|
64
|
-
| \`cchess
|
|
65
|
-
|
|
66
|
-
## Q&A
|
|
67
|
-
|
|
68
|
-
1. 如何移动棋子?
|
|
69
|
-
|
|
70
|
-
- 纵线(炮二平五/炮8平5)或字母坐标(b2e2)。
|
|
71
|
-
- 详见 [中国象棋着法表示](https://www.xqbase.com/protocol/cchess_move.htm)。
|
|
72
|
-
|
|
73
|
-
2. 人机太强/太弱?
|
|
74
|
-
|
|
75
|
-
- 调整「引擎思考深度」,深度越高棋力越强,耗时也越长。
|
|
76
|
-
|
|
77
|
-
## QQ 群
|
|
78
|
-
|
|
79
|
-
- 956758505`;
|
|
56
|
+
| \`cchess.开始 [人人/人机]\` | 开局,默认人人对战 |
|
|
57
|
+
| \`cchess.移动 <着法>\` | 落子 |
|
|
58
|
+
| \`cchess.悔棋\` | 请求悔棋 |
|
|
59
|
+
| \`cchess.悔棋 <同意/拒绝>\` | 表决悔棋,也可直接回复同意或拒绝 |
|
|
60
|
+
| \`cchess.认输\` | 认输 |
|
|
61
|
+
| \`cchess.结束\` | 强制结束棋局 |
|
|
62
|
+
| \`cchess.编辑棋盘 <FEN>\` | 导入局面,不带参数时查看 FEN 用法 |
|
|
63
|
+
| \`cchess.编辑棋盘.导出\` | 导出局面 |
|
|
64
|
+
| \`cchess.查看云库残局 [DTM/DTC]\` | 云库残局统计 |
|
|
65
|
+
| \`cchess.排行榜 [胜场/输场] [人数]\` | 排行榜 |
|
|
66
|
+
| \`cchess.查询玩家记录 [@某人]\` | 战绩 |`;
|
|
80
67
|
var inject = ["database", "puppeteer", "canvas"];
|
|
81
68
|
var pieceSkins = ["棋弈无限图形棋子", "云库木制棋子", "刘炳森红黑牛角隶书棋子", "木制棋子", "本纹隶书棋子", "银灰将军体棋子", "棋弈无限红绿正棋子", "棋弈无限红绿棋子", "棋天大圣棋子", "棋者象棋棋子", "楷书玉石棋子", "水墨青瓷棋子", "牛皮纸华康金文棋子", "皮卡鱼棋子", "红蓝祥隶棋子", "红黑精典无阴影棋子", "红黑精典棋子", "行书玉石棋子", "象甲棋子", "金属棋子篆体棋子", "镌刻华彩棋子", "鹏飞红黑棋子", "鹏飞经典棋子", "鹏飞绿龙棋子", "龙腾四海棋子"];
|
|
82
69
|
var boardSkins = ["棋弈无限红绿棋盘", "一鸣惊人棋盘", "三星堆棋盘", "云库木制棋盘", "云心鹤眼棋盘", "兰亭集序无字棋盘", "兰亭集序棋盘", "凤舞九天棋盘", "卯兔添福棋盘", "叱咤风云棋盘", "君临天下棋盘", "壁画古梦棋盘", "大闹天宫棋盘", "天山共色棋盘", "女神之约棋盘", "小吕飞刀棋盘", "山海绘卷棋盘", "护眼绿棋盘", "星河璀璨棋盘", "木制棋盘", "本纹隶书白棋盘", "本纹隶书黑棋盘", "桃花源记棋盘", "棋天大圣棋盘", "棋天无大圣棋盘", "武侠江湖棋盘", "水墨青瓷棋盘", "清悠茶道棋盘", "游园惊梦棋盘", "牛转乾坤棋盘", "玉石太极棋盘", "皓月无痕棋盘", "皮卡鱼棋盘", "盲棋神迹棋盘", "空城计棋盘", "竹波烟雨棋盘", "经典木棋盘棋盘", "绿色棋盘", "象甲2023棋盘", "象甲征程棋盘", "象甲重燃棋盘", "金牛降世棋盘", "金虎贺岁棋盘", "金虎贺岁(王天一签名版)棋盘", "鎏金岁月棋盘", "霸王别姬棋盘", "鸿门宴棋盘", "鹏飞红黑棋盘", "鹏飞经典棋盘", "鹏飞绿龙棋盘", "龙腾天坛棋盘"];
|
|
@@ -109,10 +96,10 @@ __name(isLine, "isLine");
|
|
|
109
96
|
function panel(options) {
|
|
110
97
|
const { icon = "♟️", title, at, body = [], tips = [], image } = options;
|
|
111
98
|
const lines = [`${icon} ${title}`, RULE];
|
|
112
|
-
if (at) lines.push(
|
|
99
|
+
if (at) lines.push(`@${at}`);
|
|
113
100
|
lines.push(...body.filter(isLine));
|
|
114
101
|
const validTips = tips.filter(isLine).filter(Boolean);
|
|
115
|
-
if (validTips.length) lines.push(RULE, ...validTips
|
|
102
|
+
if (validTips.length) lines.push(RULE, ...validTips);
|
|
116
103
|
if (image) lines.push(image);
|
|
117
104
|
return lines.join("\n");
|
|
118
105
|
}
|
|
@@ -214,6 +201,26 @@ function apply(ctx, config) {
|
|
|
214
201
|
primary: "id",
|
|
215
202
|
autoInc: true
|
|
216
203
|
});
|
|
204
|
+
const REGRET_AGREE = /^(同意|可以|行吧|没问题|同意悔棋|ok|yes)$/i;
|
|
205
|
+
const REGRET_REJECT = /^(拒绝|不行|不同意|不许|拒绝悔棋|no)$/i;
|
|
206
|
+
function matchRegretDecision(content) {
|
|
207
|
+
const text = content.trim().replace(/[。..!!??~~,,、;;]+$/, "");
|
|
208
|
+
if (REGRET_AGREE.test(text)) return "agree";
|
|
209
|
+
if (REGRET_REJECT.test(text)) return "reject";
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
__name(matchRegretDecision, "matchRegretDecision");
|
|
213
|
+
ctx.middleware(async (session, next) => {
|
|
214
|
+
const { channelId, content, userId } = session;
|
|
215
|
+
if (!channelId || !content) return next();
|
|
216
|
+
const decision = matchRegretDecision(content);
|
|
217
|
+
if (!decision) return next();
|
|
218
|
+
const [gameRecord] = await ctx.database.get("cchess_game_records", { channelId });
|
|
219
|
+
if (!gameRecord?.isStarted || !gameRecord.isRegretRequest) return next();
|
|
220
|
+
const [playerRecord] = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
221
|
+
if (playerRecord?.side !== convertTurnToString(gameRecord.turn)) return next();
|
|
222
|
+
await resolveRegret(session, decision === "agree");
|
|
223
|
+
});
|
|
217
224
|
ctx.middleware(async (session, next) => {
|
|
218
225
|
const { channelId, content, userId, username } = session;
|
|
219
226
|
if (!channelId || !content) return next();
|
|
@@ -242,21 +249,21 @@ function apply(ctx, config) {
|
|
|
242
249
|
const gameRecord = await getGameRecord(channelId);
|
|
243
250
|
if (gameRecord.isStarted) {
|
|
244
251
|
return await sendMessage(session, panel({
|
|
245
|
-
icon: "
|
|
252
|
+
icon: "⚠️",
|
|
246
253
|
title: "棋局已经开战",
|
|
247
254
|
at: username,
|
|
248
|
-
body: ["
|
|
249
|
-
tips: ["
|
|
255
|
+
body: ["本局正在进行,无法中途加入。"],
|
|
256
|
+
tips: ["本局结束后可再加入。"],
|
|
250
257
|
image: await renderBoard(channelId)
|
|
251
258
|
}));
|
|
252
259
|
}
|
|
253
260
|
void checkEngine(channelId);
|
|
254
261
|
if (choice?.includes("红") && choice.includes("黑")) {
|
|
255
262
|
return await sendMessage(session, panel({
|
|
256
|
-
icon: "
|
|
263
|
+
icon: "⚠️",
|
|
257
264
|
title: "一次只能选一方",
|
|
258
265
|
at: username,
|
|
259
|
-
body: ["
|
|
266
|
+
body: ["请只选择红方或黑方。"],
|
|
260
267
|
tips: ["「cchess.加入 红」执红先行", "「cchess.加入 黑」执黑应招"]
|
|
261
268
|
}));
|
|
262
269
|
}
|
|
@@ -269,16 +276,16 @@ function apply(ctx, config) {
|
|
|
269
276
|
if (selfRecords.length === 0) {
|
|
270
277
|
await ctx.database.create("cchess_gaming_player_records", { channelId, userId, username, side: choice });
|
|
271
278
|
return await sendMessage(session, panel({
|
|
272
|
-
icon: "
|
|
279
|
+
icon: "✅",
|
|
273
280
|
title: "入座成功",
|
|
274
281
|
at: username,
|
|
275
282
|
body: [field("阵营", withSideIcon(choice)), field("当前人数", `${playersNum + 1} 人`)],
|
|
276
|
-
tips: ["再次发送「cchess.加入 红 / 黑」即可换边", "人齐后发送「cchess
|
|
283
|
+
tips: ["再次发送「cchess.加入 红 / 黑」即可换边", "人齐后发送「cchess.开始 人人」开局"]
|
|
277
284
|
}));
|
|
278
285
|
} else {
|
|
279
286
|
await ctx.database.set("cchess_gaming_player_records", { channelId, userId }, { side: choice });
|
|
280
287
|
return await sendMessage(session, panel({
|
|
281
|
-
icon: "
|
|
288
|
+
icon: "✅",
|
|
282
289
|
title: "换边成功",
|
|
283
290
|
at: username,
|
|
284
291
|
body: [field("新的阵营", withSideIcon(choice)), field("当前人数", `${playersNum} 人`)]
|
|
@@ -291,11 +298,11 @@ function apply(ctx, config) {
|
|
|
291
298
|
const gameRecord = await getGameRecord(channelId);
|
|
292
299
|
if (gameRecord.isStarted) {
|
|
293
300
|
return await sendMessage(session, panel({
|
|
294
|
-
icon: "
|
|
301
|
+
icon: "⚠️",
|
|
295
302
|
title: "棋局已经开战",
|
|
296
303
|
at: username,
|
|
297
|
-
body: ["
|
|
298
|
-
tips: ["
|
|
304
|
+
body: ["对局进行中,无法退出。"],
|
|
305
|
+
tips: ["可发送「cchess.认输」结束本局"]
|
|
299
306
|
}));
|
|
300
307
|
}
|
|
301
308
|
const selfRecords = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
@@ -306,32 +313,32 @@ function apply(ctx, config) {
|
|
|
306
313
|
} else {
|
|
307
314
|
await ctx.database.remove("cchess_gaming_player_records", { channelId, userId });
|
|
308
315
|
return await sendMessage(session, panel({
|
|
309
|
-
icon: "
|
|
316
|
+
icon: "✅",
|
|
310
317
|
title: "已离席",
|
|
311
318
|
at: username,
|
|
312
|
-
body: ["
|
|
319
|
+
body: ["已退出本局。", field("剩余人数", `${playersNum - 1} 人`)]
|
|
313
320
|
}));
|
|
314
321
|
}
|
|
315
322
|
});
|
|
316
|
-
ctx.command("cchess.开始", "
|
|
317
|
-
await session.execute(`cchess.开始 -h`);
|
|
318
|
-
});
|
|
319
|
-
ctx.command("cchess.开始.人人对战", "开始人人对战").action(async ({ session }) => {
|
|
323
|
+
ctx.command("cchess.开始 [mode:string]", "开始对局,mode 可选「人人」或「人机」").action(async ({ session }, mode) => {
|
|
320
324
|
const { username, userId, channelId } = session;
|
|
321
325
|
await updateNameInPlayerRecord(userId, username);
|
|
322
326
|
const gameRecord = await getGameRecord(channelId);
|
|
323
327
|
if (gameRecord.isStarted) {
|
|
324
328
|
return await sendMessage(session, alreadyStartedPanel(username));
|
|
325
329
|
}
|
|
326
|
-
void checkEngine(channelId);
|
|
327
330
|
const playerRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
328
331
|
const playersNum = playerRecords.length;
|
|
332
|
+
if (/机|电脑|ai|engine|pve/i.test(mode ?? "")) {
|
|
333
|
+
return await startVersusEngine(session, gameRecord, playerRecords);
|
|
334
|
+
}
|
|
335
|
+
void checkEngine(channelId);
|
|
329
336
|
let redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
330
337
|
let blackPlayers = playerRecords.filter((player) => player.side === "黑方");
|
|
331
338
|
let assignNotice = "";
|
|
332
339
|
if (playersNum < 2) {
|
|
333
340
|
return await sendMessage(session, panel({
|
|
334
|
-
icon: "
|
|
341
|
+
icon: "⚠️",
|
|
335
342
|
title: "棋差一手,人差一位",
|
|
336
343
|
at: username,
|
|
337
344
|
body: ["人人对战至少需要 2 位棋手。", field("当前人数", `${playersNum} 人`)],
|
|
@@ -352,7 +359,7 @@ function apply(ctx, config) {
|
|
|
352
359
|
await ctx.database.set("cchess_game_records", { channelId }, { isStarted: true });
|
|
353
360
|
const sideString = convertTurnToString(gameRecord.turn);
|
|
354
361
|
return await sendMessage(session, panel({
|
|
355
|
-
icon: "
|
|
362
|
+
icon: "✅",
|
|
356
363
|
title: "楚河汉界,对局开始",
|
|
357
364
|
body: [
|
|
358
365
|
...assignNotice ? [assignNotice, ""] : [],
|
|
@@ -368,69 +375,13 @@ function apply(ctx, config) {
|
|
|
368
375
|
image: await renderBoard(channelId)
|
|
369
376
|
}));
|
|
370
377
|
});
|
|
371
|
-
ctx.command("cchess.开始.人机对战", "开始人机对战").action(async ({ session }) => {
|
|
372
|
-
const { username, userId, channelId } = session;
|
|
373
|
-
await updateNameInPlayerRecord(userId, username);
|
|
374
|
-
const gameRecord = await getGameRecord(channelId);
|
|
375
|
-
if (gameRecord.isStarted) {
|
|
376
|
-
return await sendMessage(session, alreadyStartedPanel(username));
|
|
377
|
-
}
|
|
378
|
-
const playerRecords = await ctx.database.get("cchess_gaming_player_records", { channelId });
|
|
379
|
-
const playersNum = playerRecords.length;
|
|
380
|
-
const redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
381
|
-
const blackPlayers = playerRecords.filter((player) => player.side === "黑方");
|
|
382
|
-
if (playersNum < 1 && !config.allowFreePieceMovementInHumanMachineMode) {
|
|
383
|
-
return await sendMessage(session, panel({
|
|
384
|
-
icon: "🪑",
|
|
385
|
-
title: "尚无棋手入座",
|
|
386
|
-
at: username,
|
|
387
|
-
body: ["人机对战至少需要 1 位棋手。"],
|
|
388
|
-
tips: ["发送「cchess.加入」即可入座挑战皮卡鱼"]
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
if (!await checkEngine(channelId)) {
|
|
392
|
-
return await sendMessage(session, engineUnavailablePanel(username));
|
|
393
|
-
}
|
|
394
|
-
let humanSide;
|
|
395
|
-
if (playersNum === 1) {
|
|
396
|
-
humanSide = playerRecords[0].side === "红方" ? "红方" : "黑方";
|
|
397
|
-
} else if (redPlayers.length !== blackPlayers.length) {
|
|
398
|
-
humanSide = redPlayers.length > blackPlayers.length ? "红方" : "黑方";
|
|
399
|
-
} else {
|
|
400
|
-
humanSide = Math.random() < 0.5 ? "红方" : "黑方";
|
|
401
|
-
}
|
|
402
|
-
const engineSide = humanSide === "红方" ? "黑方" : "红方";
|
|
403
|
-
for (const player of playerRecords) {
|
|
404
|
-
if (player.side === humanSide) continue;
|
|
405
|
-
await ctx.database.set("cchess_gaming_player_records", { channelId, userId: player.userId }, { side: humanSide });
|
|
406
|
-
}
|
|
407
|
-
await ctx.database.set("cchess_game_records", { channelId }, {
|
|
408
|
-
isStarted: true,
|
|
409
|
-
isEnginePlayRed: engineSide === "红方",
|
|
410
|
-
isEnginePlayBlack: engineSide === "黑方"
|
|
411
|
-
});
|
|
412
|
-
const sideString = convertTurnToString(gameRecord.turn);
|
|
413
|
-
if (engineSide === sideString) await requestEngineMove(channelId);
|
|
414
|
-
return await sendMessage(session, panel({
|
|
415
|
-
icon: "🎉",
|
|
416
|
-
title: "人机对局开始",
|
|
417
|
-
body: [
|
|
418
|
-
field("棋 手", withSideIcon(humanSide)),
|
|
419
|
-
field("皮卡鱼", withSideIcon(engineSide)),
|
|
420
|
-
field("先 手", withSideIcon(sideString)),
|
|
421
|
-
field("思考深度", `${thinkingDepth} 层`)
|
|
422
|
-
],
|
|
423
|
-
tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
|
|
424
|
-
image: await renderBoard(channelId)
|
|
425
|
-
}));
|
|
426
|
-
});
|
|
427
378
|
ctx.command("cchess.结束", "强制结束游戏").action(async ({ session }) => {
|
|
428
379
|
const { username, userId, channelId } = session;
|
|
429
380
|
await updateNameInPlayerRecord(userId, username);
|
|
430
381
|
const gameRecord = await getGameRecord(channelId);
|
|
431
382
|
if (!gameRecord.isStarted) {
|
|
432
383
|
return await sendMessage(session, panel({
|
|
433
|
-
icon: "
|
|
384
|
+
icon: "⚠️",
|
|
434
385
|
title: "棋盘尚且空空",
|
|
435
386
|
at: username,
|
|
436
387
|
body: ["当前没有进行中的对局,无需收拾残局。"],
|
|
@@ -439,7 +390,7 @@ function apply(ctx, config) {
|
|
|
439
390
|
}
|
|
440
391
|
await endGame(channelId);
|
|
441
392
|
return await sendMessage(session, panel({
|
|
442
|
-
icon: "
|
|
393
|
+
icon: "✅",
|
|
443
394
|
title: "对局已强制结束",
|
|
444
395
|
at: username,
|
|
445
396
|
body: ["棋盘已收,胜负不计。"],
|
|
@@ -473,7 +424,7 @@ function apply(ctx, config) {
|
|
|
473
424
|
title: "悔棋请求待决",
|
|
474
425
|
at: username,
|
|
475
426
|
body: ["对方的悔棋请求尚未答复,棋局暂歇。"],
|
|
476
|
-
tips: ["
|
|
427
|
+
tips: ["对方回复「同意」或「拒绝」即可表决"]
|
|
477
428
|
}));
|
|
478
429
|
}
|
|
479
430
|
const turn = gameRecord.turn;
|
|
@@ -569,12 +520,20 @@ function apply(ctx, config) {
|
|
|
569
520
|
busyChannels.delete(channelId);
|
|
570
521
|
}
|
|
571
522
|
});
|
|
572
|
-
ctx.command("cchess.悔棋", "
|
|
573
|
-
await session.execute(`cchess.悔棋 -h`);
|
|
574
|
-
});
|
|
575
|
-
ctx.command("cchess.悔棋.请求", "请求悔棋").action(async ({ session }) => {
|
|
523
|
+
ctx.command("cchess.悔棋 [decision:string]", "请求悔棋;待决时由对方以「同意 / 拒绝」表决").action(async ({ session }, decision) => {
|
|
576
524
|
const { username, userId, channelId } = session;
|
|
577
525
|
await updateNameInPlayerRecord(userId, username);
|
|
526
|
+
if (decision) {
|
|
527
|
+
const choice = matchRegretDecision(decision);
|
|
528
|
+
if (choice) return await resolveRegret(session, choice === "agree");
|
|
529
|
+
return await sendMessage(session, panel({
|
|
530
|
+
icon: "⚠️",
|
|
531
|
+
title: "表决无效",
|
|
532
|
+
at: username,
|
|
533
|
+
body: ["悔棋表决只有同意与拒绝两种。"],
|
|
534
|
+
tips: ["「cchess.悔棋 同意」或「cchess.悔棋 拒绝」"]
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
578
537
|
const gameRecord = await getGameRecord(channelId);
|
|
579
538
|
if (!gameRecord.isStarted) {
|
|
580
539
|
return await sendMessage(session, notStartedPanel(username));
|
|
@@ -587,7 +546,8 @@ function apply(ctx, config) {
|
|
|
587
546
|
icon: "⏳",
|
|
588
547
|
title: "已有悔棋请求",
|
|
589
548
|
at: username,
|
|
590
|
-
body: ["正在等待对方答复,请勿重复请求。"]
|
|
549
|
+
body: ["正在等待对方答复,请勿重复请求。"],
|
|
550
|
+
tips: ["对方回复「同意」或「拒绝」即可表决"]
|
|
591
551
|
}));
|
|
592
552
|
}
|
|
593
553
|
const playerRecord = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
@@ -596,7 +556,7 @@ function apply(ctx, config) {
|
|
|
596
556
|
}
|
|
597
557
|
if (gameRecord.moveList.length < 1) {
|
|
598
558
|
return await sendMessage(session, panel({
|
|
599
|
-
icon: "
|
|
559
|
+
icon: "⚠️",
|
|
600
560
|
title: "棋局初开",
|
|
601
561
|
at: username,
|
|
602
562
|
body: ["一子未落,无悔可言。"]
|
|
@@ -607,7 +567,7 @@ function apply(ctx, config) {
|
|
|
607
567
|
await undoMove(channelId);
|
|
608
568
|
await undoMove(channelId);
|
|
609
569
|
return await sendMessage(session, panel({
|
|
610
|
-
icon: "
|
|
570
|
+
icon: "✅",
|
|
611
571
|
title: "悔棋成功",
|
|
612
572
|
at: username,
|
|
613
573
|
body: ["棋子已归原位,皮卡鱼宽宏大量。"],
|
|
@@ -616,48 +576,20 @@ function apply(ctx, config) {
|
|
|
616
576
|
}
|
|
617
577
|
if (playerRecord[0].side === sideString) {
|
|
618
578
|
return await sendMessage(session, panel({
|
|
619
|
-
icon: "
|
|
579
|
+
icon: "⚠️",
|
|
620
580
|
title: "此刻不可悔棋",
|
|
621
581
|
at: username,
|
|
622
|
-
body: ["
|
|
582
|
+
body: ["轮到你落子,上一着是对方所走。"],
|
|
623
583
|
tips: ["落子之后,方有悔棋之说"]
|
|
624
584
|
}));
|
|
625
585
|
}
|
|
626
586
|
await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: true });
|
|
627
587
|
return await sendMessage(session, panel({
|
|
628
|
-
icon: "
|
|
588
|
+
icon: "⏳",
|
|
629
589
|
title: "悔棋请求已送出",
|
|
630
590
|
at: username,
|
|
631
591
|
body: [field("等待答复", withSideIcon(sideString))],
|
|
632
|
-
tips: ["
|
|
633
|
-
}));
|
|
634
|
-
});
|
|
635
|
-
ctx.command("cchess.悔棋.同意", "同意悔棋").action(async ({ session }) => {
|
|
636
|
-
const { username, channelId } = session;
|
|
637
|
-
const decision = await checkRegretDecision(session, "同意");
|
|
638
|
-
if (typeof decision === "string") return await sendMessage(session, decision);
|
|
639
|
-
await undoMove(channelId);
|
|
640
|
-
await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
|
|
641
|
-
return await sendMessage(session, panel({
|
|
642
|
-
icon: "🤝",
|
|
643
|
-
title: "悔棋成功",
|
|
644
|
-
at: username,
|
|
645
|
-
body: ["您应允了对方的请求,棋子已归原位。"],
|
|
646
|
-
tips: ["棋风谦和,可敬可佩"],
|
|
647
|
-
image: await renderBoard(channelId)
|
|
648
|
-
}));
|
|
649
|
-
});
|
|
650
|
-
ctx.command("cchess.悔棋.拒绝", "拒绝悔棋").action(async ({ session }) => {
|
|
651
|
-
const { username, channelId } = session;
|
|
652
|
-
const decision = await checkRegretDecision(session, "拒绝");
|
|
653
|
-
if (typeof decision === "string") return await sendMessage(session, decision);
|
|
654
|
-
await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
|
|
655
|
-
return await sendMessage(session, panel({
|
|
656
|
-
icon: "✋",
|
|
657
|
-
title: "悔棋被拒",
|
|
658
|
-
at: username,
|
|
659
|
-
body: ["落子无悔,棋局继续。"],
|
|
660
|
-
image: await renderBoard(channelId)
|
|
592
|
+
tips: ["对方回复「同意」或「拒绝」即可表决"]
|
|
661
593
|
}));
|
|
662
594
|
});
|
|
663
595
|
ctx.command("cchess.认输", "认输").action(async ({ session }) => {
|
|
@@ -678,53 +610,46 @@ function apply(ctx, config) {
|
|
|
678
610
|
const sideString = convertTurnToString(turn);
|
|
679
611
|
if (playerRecord[0].side !== sideString) {
|
|
680
612
|
return await sendMessage(session, panel({
|
|
681
|
-
icon: "
|
|
613
|
+
icon: "⚠️",
|
|
682
614
|
title: "此刻不便认输",
|
|
683
615
|
at: username,
|
|
684
|
-
body: [field("
|
|
685
|
-
tips: ["
|
|
616
|
+
body: [field("你的阵营", withSideIcon(playerRecord[0].side)), field("当前轮走", withSideIcon(sideString))],
|
|
617
|
+
tips: ["轮到你走棋时,才可以认输"]
|
|
686
618
|
}));
|
|
687
619
|
}
|
|
688
620
|
const message = await settleVictory(channelId, turn === "w" ? "b" : "w", turn, "拱手认负", {
|
|
689
|
-
icon: "
|
|
621
|
+
icon: "✅",
|
|
690
622
|
at: username,
|
|
691
623
|
extra: [`${withSideIcon(sideString)} 推枰认负,风度翩翩。`]
|
|
692
624
|
});
|
|
693
625
|
return await sendMessage(session, message);
|
|
694
626
|
});
|
|
695
|
-
ctx.command("cchess.查看云库残局", "
|
|
696
|
-
await session.execute(`cchess.查看云库残局 -h`);
|
|
697
|
-
});
|
|
698
|
-
ctx.command("cchess.查看云库残局.DTM统计", "云库残局 DTM 统计").action(async ({ session }) => {
|
|
699
|
-
const { username, userId } = session;
|
|
700
|
-
await updateNameInPlayerRecord(userId, username);
|
|
701
|
-
return await sendMessage(session, panel({
|
|
702
|
-
icon: "☁️",
|
|
703
|
-
title: "云库残局 · DTM 统计",
|
|
704
|
-
at: username,
|
|
705
|
-
body: ["DTM:距将死的步数统计。", "https://www.chessdb.cn/egtb_info_dtm.html"]
|
|
706
|
-
}));
|
|
707
|
-
});
|
|
708
|
-
ctx.command("cchess.查看云库残局.DTC统计", "云库残局 DTC 统计").action(async ({ session }) => {
|
|
627
|
+
ctx.command("cchess.查看云库残局 [type:string]", "云库残局 DTM / DTC 统计").action(async ({ session }, type) => {
|
|
709
628
|
const { username, userId } = session;
|
|
710
629
|
await updateNameInPlayerRecord(userId, username);
|
|
630
|
+
const isDtc = /dtc/i.test(type ?? "");
|
|
711
631
|
return await sendMessage(session, panel({
|
|
712
|
-
icon: "
|
|
713
|
-
title:
|
|
632
|
+
icon: "📋",
|
|
633
|
+
title: `云库残局 · ${isDtc ? "DTC" : "DTM"} 统计`,
|
|
714
634
|
at: username,
|
|
715
|
-
body: ["DTC:距吃子的步数统计。", "https://www.chessdb.cn/egtb_info.html"]
|
|
635
|
+
body: isDtc ? ["DTC:距吃子的步数统计。", "https://www.chessdb.cn/egtb_info.html"] : ["DTM:距将死的步数统计。", "https://www.chessdb.cn/egtb_info_dtm.html"]
|
|
716
636
|
}));
|
|
717
637
|
});
|
|
718
|
-
ctx.command("cchess.编辑棋盘", "
|
|
719
|
-
await session.execute(`cchess.编辑棋盘 -h`);
|
|
720
|
-
});
|
|
721
|
-
ctx.command("cchess.编辑棋盘.导入 <fen:text>", "导入FEN串").action(async ({ session }, fen) => {
|
|
638
|
+
ctx.command("cchess.编辑棋盘 [fen:text]", "导入 FEN 局面,不带参数时查看 FEN 用法").action(async ({ session }, fen) => {
|
|
722
639
|
const { username, userId, channelId } = session;
|
|
723
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
|
+
}
|
|
724
649
|
const gameRecord = await getGameRecord(channelId);
|
|
725
650
|
if (gameRecord.isStarted) {
|
|
726
651
|
return await sendMessage(session, panel({
|
|
727
|
-
icon: "
|
|
652
|
+
icon: "⚠️",
|
|
728
653
|
title: "棋局进行中",
|
|
729
654
|
at: username,
|
|
730
655
|
body: ["对弈期间不可改动棋盘。"],
|
|
@@ -734,13 +659,13 @@ function apply(ctx, config) {
|
|
|
734
659
|
fen = fen?.trim();
|
|
735
660
|
if (!fen || !isValidateFen(fen)) {
|
|
736
661
|
return await sendMessage(session, panel({
|
|
737
|
-
icon: "
|
|
662
|
+
icon: "❌",
|
|
738
663
|
title: "FEN 串无法解析",
|
|
739
664
|
at: username,
|
|
740
665
|
body: ["请检查局面串的格式是否完整。"],
|
|
741
666
|
tips: [
|
|
742
667
|
"示例:rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w",
|
|
743
|
-
"发送「cchess
|
|
668
|
+
"发送「cchess.编辑棋盘」查看规范"
|
|
744
669
|
]
|
|
745
670
|
}));
|
|
746
671
|
}
|
|
@@ -760,11 +685,11 @@ function apply(ctx, config) {
|
|
|
760
685
|
await parseFen(channelId, fen);
|
|
761
686
|
const record = await getGameRecord(channelId);
|
|
762
687
|
return await sendMessage(session, panel({
|
|
763
|
-
icon: "
|
|
688
|
+
icon: "✅",
|
|
764
689
|
title: "棋盘摆放完毕",
|
|
765
690
|
at: username,
|
|
766
691
|
body: [field("轮走方", withSideIcon(convertTurnToString(record.turn)))],
|
|
767
|
-
tips: ["发送「cchess
|
|
692
|
+
tips: ["发送「cchess.开始 人人」或「cchess.开始 人机」由此局面开战"],
|
|
768
693
|
image: await renderBoard(channelId)
|
|
769
694
|
}));
|
|
770
695
|
});
|
|
@@ -780,41 +705,31 @@ function apply(ctx, config) {
|
|
|
780
705
|
}
|
|
781
706
|
const fenWithFullMove = await getFenWithFullMove(channelId);
|
|
782
707
|
return await sendMessage(session, panel({
|
|
783
|
-
icon: "
|
|
708
|
+
icon: "📋",
|
|
784
709
|
title: "当前局面 FEN",
|
|
785
710
|
at: username,
|
|
786
711
|
body: [fenWithFullMove],
|
|
787
|
-
tips: ["可用「cchess
|
|
712
|
+
tips: ["可用「cchess.编辑棋盘 <FEN>」还原此局面"]
|
|
788
713
|
}));
|
|
789
714
|
});
|
|
790
|
-
ctx.command("cchess
|
|
791
|
-
const { username, userId } = session;
|
|
792
|
-
await updateNameInPlayerRecord(userId, username);
|
|
793
|
-
return await sendMessage(session, panel({
|
|
794
|
-
icon: "📖",
|
|
795
|
-
title: "FEN 串使用方法",
|
|
796
|
-
at: username,
|
|
797
|
-
body: ["中国象棋 FEN 格式规范:", "https://www.xqbase.com/protocol/cchess_fen.htm"]
|
|
798
|
-
}));
|
|
799
|
-
});
|
|
800
|
-
ctx.command("cchess.排行榜", "排行榜指令帮助").action(async ({ session }) => {
|
|
801
|
-
await session.execute(`cchess.排行榜 -h`);
|
|
802
|
-
});
|
|
803
|
-
ctx.command("cchess.排行榜.总胜场 [number:number]", "查看玩家总胜场排行榜").action(async ({ session }, number = config.defaultMaxLeaderboardEntries) => {
|
|
715
|
+
ctx.command("cchess.排行榜 [type:string] [number:number]", "查看玩家排行榜").action(async ({ session }, type, number = config.defaultMaxLeaderboardEntries) => {
|
|
804
716
|
const { userId, username } = session;
|
|
805
717
|
await updateNameInPlayerRecord(userId, username);
|
|
806
|
-
if (
|
|
807
|
-
|
|
718
|
+
if (type && /^\d+$/.test(type)) {
|
|
719
|
+
number = Number(type);
|
|
720
|
+
type = "";
|
|
808
721
|
}
|
|
809
|
-
return await getLeaderboard(session, "win", "总胜场排行榜", "🏆", number);
|
|
810
|
-
});
|
|
811
|
-
ctx.command("cchess.排行榜.总输场 [number:number]", "查看玩家总输场排行榜").action(async ({ session }, number = config.defaultMaxLeaderboardEntries) => {
|
|
812
|
-
const { userId, username } = session;
|
|
813
|
-
await updateNameInPlayerRecord(userId, username);
|
|
814
722
|
if (typeof number !== "number" || isNaN(number) || number < 0) {
|
|
815
723
|
return await sendMessage(session, invalidLeaderboardSizePanel(username));
|
|
816
724
|
}
|
|
817
|
-
|
|
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
|
+
);
|
|
818
733
|
});
|
|
819
734
|
ctx.command("cchess.查询玩家记录 [targetUser:text]", "查询玩家记录").action(async ({ session }, targetUser) => {
|
|
820
735
|
let { userId, username } = session;
|
|
@@ -860,37 +775,37 @@ function apply(ctx, config) {
|
|
|
860
775
|
});
|
|
861
776
|
function notStartedPanel(username) {
|
|
862
777
|
return panel({
|
|
863
|
-
icon: "
|
|
778
|
+
icon: "⚠️",
|
|
864
779
|
title: "棋局尚未开始",
|
|
865
780
|
at: username,
|
|
866
781
|
body: ["当前频道还没有进行中的对局。"],
|
|
867
|
-
tips: ["「cchess.加入」入座", "「cchess
|
|
782
|
+
tips: ["「cchess.加入」入座", "「cchess.开始 人人」与好友手谈", "「cchess.开始 人机」挑战皮卡鱼"]
|
|
868
783
|
});
|
|
869
784
|
}
|
|
870
785
|
__name(notStartedPanel, "notStartedPanel");
|
|
871
786
|
function alreadyStartedPanel(username) {
|
|
872
787
|
return panel({
|
|
873
|
-
icon: "
|
|
788
|
+
icon: "⚠️",
|
|
874
789
|
title: "棋局已经开战",
|
|
875
790
|
at: username,
|
|
876
|
-
body: ["
|
|
877
|
-
tips: ["「cchess
|
|
791
|
+
body: ["本局尚未结束,不必重开。"],
|
|
792
|
+
tips: ["「cchess.认输」结束本局", "「cchess.结束」强制清盘"]
|
|
878
793
|
});
|
|
879
794
|
}
|
|
880
795
|
__name(alreadyStartedPanel, "alreadyStartedPanel");
|
|
881
796
|
function notJoinedPanel(username) {
|
|
882
797
|
return panel({
|
|
883
|
-
icon: "
|
|
884
|
-
title: "
|
|
798
|
+
icon: "⚠️",
|
|
799
|
+
title: "你尚未入座",
|
|
885
800
|
at: username,
|
|
886
|
-
body: ["
|
|
801
|
+
body: ["请先加入对局。"],
|
|
887
802
|
tips: ["发送「cchess.加入」加入对局"]
|
|
888
803
|
});
|
|
889
804
|
}
|
|
890
805
|
__name(notJoinedPanel, "notJoinedPanel");
|
|
891
806
|
function analyzingPanel(username) {
|
|
892
807
|
return panel({
|
|
893
|
-
icon: "
|
|
808
|
+
icon: "⏳",
|
|
894
809
|
title: "皮卡鱼正在推演",
|
|
895
810
|
at: username,
|
|
896
811
|
body: ["引擎正在计算局面,请稍候再试。"]
|
|
@@ -899,17 +814,17 @@ function apply(ctx, config) {
|
|
|
899
814
|
__name(analyzingPanel, "analyzingPanel");
|
|
900
815
|
function notYourTurnPanel(username, yourSide, currentSide) {
|
|
901
816
|
return panel({
|
|
902
|
-
icon: "
|
|
903
|
-
title: "
|
|
817
|
+
icon: "⚠️",
|
|
818
|
+
title: "尚未轮到你",
|
|
904
819
|
at: username,
|
|
905
|
-
body: [field("
|
|
906
|
-
tips: ["
|
|
820
|
+
body: [field("你的阵营", withSideIcon(yourSide)), field("当前轮走", withSideIcon(currentSide))],
|
|
821
|
+
tips: ["请等待对方落子。"]
|
|
907
822
|
});
|
|
908
823
|
}
|
|
909
824
|
__name(notYourTurnPanel, "notYourTurnPanel");
|
|
910
825
|
function invalidMovePanel(username, reason) {
|
|
911
826
|
return panel({
|
|
912
|
-
icon: "
|
|
827
|
+
icon: "❌",
|
|
913
828
|
title: reason,
|
|
914
829
|
at: username,
|
|
915
830
|
body: ["请确认着法是否合乎棋规。"],
|
|
@@ -919,21 +834,21 @@ function apply(ctx, config) {
|
|
|
919
834
|
__name(invalidMovePanel, "invalidMovePanel");
|
|
920
835
|
function engineUnavailablePanel(username) {
|
|
921
836
|
return panel({
|
|
922
|
-
icon: "
|
|
837
|
+
icon: "⏳",
|
|
923
838
|
title: "皮卡鱼尚未就绪",
|
|
924
839
|
at: username,
|
|
925
840
|
body: ["引擎启动失败,暂时无法进行人机对战。"],
|
|
926
|
-
tips: ["稍后重试,或改用「cchess
|
|
841
|
+
tips: ["稍后重试,或改用「cchess.开始 人人」"]
|
|
927
842
|
});
|
|
928
843
|
}
|
|
929
844
|
__name(engineUnavailablePanel, "engineUnavailablePanel");
|
|
930
845
|
function invalidLeaderboardSizePanel(username) {
|
|
931
846
|
return panel({
|
|
932
|
-
icon: "
|
|
847
|
+
icon: "⚠️",
|
|
933
848
|
title: "榜单人数有误",
|
|
934
849
|
at: username,
|
|
935
|
-
body: ["
|
|
936
|
-
tips: ["例如:cchess
|
|
850
|
+
body: ["请输入不小于 0 的整数。"],
|
|
851
|
+
tips: ["例如:cchess.排行榜 胜场 10"]
|
|
937
852
|
});
|
|
938
853
|
}
|
|
939
854
|
__name(invalidLeaderboardSizePanel, "invalidLeaderboardSizePanel");
|
|
@@ -942,7 +857,7 @@ function apply(ctx, config) {
|
|
|
942
857
|
}
|
|
943
858
|
__name(renderBoard, "renderBoard");
|
|
944
859
|
async function settleVictory(channelId, winSide, loseSide, title, options = {}) {
|
|
945
|
-
const { icon = "
|
|
860
|
+
const { icon = "✅", at, extra = [], withBoard = false } = options;
|
|
946
861
|
const image = withBoard ? await renderBoard(channelId) : void 0;
|
|
947
862
|
const winners = await ctx.database.get("cchess_gaming_player_records", {
|
|
948
863
|
channelId,
|
|
@@ -964,34 +879,104 @@ function apply(ctx, config) {
|
|
|
964
879
|
});
|
|
965
880
|
}
|
|
966
881
|
__name(settleVictory, "settleVictory");
|
|
967
|
-
async function
|
|
882
|
+
async function startVersusEngine(session, gameRecord, playerRecords) {
|
|
883
|
+
const { username, channelId } = session;
|
|
884
|
+
const playersNum = playerRecords.length;
|
|
885
|
+
const redPlayers = playerRecords.filter((player) => player.side === "红方");
|
|
886
|
+
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
|
+
if (!await checkEngine(channelId)) {
|
|
897
|
+
return await sendMessage(session, engineUnavailablePanel(username));
|
|
898
|
+
}
|
|
899
|
+
let humanSide;
|
|
900
|
+
if (playersNum === 1) {
|
|
901
|
+
humanSide = playerRecords[0].side === "红方" ? "红方" : "黑方";
|
|
902
|
+
} else if (redPlayers.length !== blackPlayers.length) {
|
|
903
|
+
humanSide = redPlayers.length > blackPlayers.length ? "红方" : "黑方";
|
|
904
|
+
} else {
|
|
905
|
+
humanSide = Math.random() < 0.5 ? "红方" : "黑方";
|
|
906
|
+
}
|
|
907
|
+
const engineSide = humanSide === "红方" ? "黑方" : "红方";
|
|
908
|
+
for (const player of playerRecords) {
|
|
909
|
+
if (player.side === humanSide) continue;
|
|
910
|
+
await ctx.database.set("cchess_gaming_player_records", { channelId, userId: player.userId }, { side: humanSide });
|
|
911
|
+
}
|
|
912
|
+
await ctx.database.set("cchess_game_records", { channelId }, {
|
|
913
|
+
isStarted: true,
|
|
914
|
+
isEnginePlayRed: engineSide === "红方",
|
|
915
|
+
isEnginePlayBlack: engineSide === "黑方"
|
|
916
|
+
});
|
|
917
|
+
const sideString = convertTurnToString(gameRecord.turn);
|
|
918
|
+
if (engineSide === sideString) await requestEngineMove(channelId);
|
|
919
|
+
return await sendMessage(session, panel({
|
|
920
|
+
icon: "✅",
|
|
921
|
+
title: "人机对局开始",
|
|
922
|
+
body: [
|
|
923
|
+
field("棋 手", withSideIcon(humanSide)),
|
|
924
|
+
field("皮卡鱼", withSideIcon(engineSide)),
|
|
925
|
+
field("先 手", withSideIcon(sideString)),
|
|
926
|
+
field("思考深度", `${thinkingDepth} 层`)
|
|
927
|
+
],
|
|
928
|
+
tips: ["直接发送着法即可落子,如「炮二平五」或「b2e2」"],
|
|
929
|
+
image: await renderBoard(channelId)
|
|
930
|
+
}));
|
|
931
|
+
}
|
|
932
|
+
__name(startVersusEngine, "startVersusEngine");
|
|
933
|
+
async function resolveRegret(session, agree) {
|
|
968
934
|
const { username, userId, channelId } = session;
|
|
969
935
|
await updateNameInPlayerRecord(userId, username);
|
|
970
936
|
const gameRecord = await getGameRecord(channelId);
|
|
971
|
-
if (!gameRecord.isStarted) return notStartedPanel(username);
|
|
972
|
-
if (gameRecord.isAnalyzing || busyChannels.has(channelId)) return analyzingPanel(username);
|
|
937
|
+
if (!gameRecord.isStarted) return await sendMessage(session, notStartedPanel(username));
|
|
938
|
+
if (gameRecord.isAnalyzing || busyChannels.has(channelId)) return await sendMessage(session, analyzingPanel(username));
|
|
973
939
|
if (!gameRecord.isRegretRequest) {
|
|
974
|
-
return panel({
|
|
975
|
-
icon: "
|
|
940
|
+
return await sendMessage(session, panel({
|
|
941
|
+
icon: "⚠️",
|
|
976
942
|
title: "暂无悔棋请求",
|
|
977
943
|
at: username,
|
|
978
|
-
body: [`当前没有待答复的请求,无从${
|
|
979
|
-
});
|
|
944
|
+
body: [`当前没有待答复的请求,无从${agree ? "同意" : "拒绝"}。`]
|
|
945
|
+
}));
|
|
980
946
|
}
|
|
981
947
|
const playerRecord = await ctx.database.get("cchess_gaming_player_records", { channelId, userId });
|
|
982
|
-
if (playerRecord.length === 0) return notJoinedPanel(username);
|
|
948
|
+
if (playerRecord.length === 0) return await sendMessage(session, notJoinedPanel(username));
|
|
983
949
|
const sideString = convertTurnToString(gameRecord.turn);
|
|
984
950
|
if (playerRecord[0].side !== sideString) {
|
|
985
|
-
return panel({
|
|
986
|
-
icon: "
|
|
951
|
+
return await sendMessage(session, panel({
|
|
952
|
+
icon: "⚠️",
|
|
987
953
|
title: "无权表决",
|
|
988
954
|
at: username,
|
|
989
955
|
body: ["悔棋请求正由对方等待答复。", field("应答方", withSideIcon(sideString))]
|
|
990
|
-
});
|
|
956
|
+
}));
|
|
991
957
|
}
|
|
992
|
-
|
|
958
|
+
if (!agree) {
|
|
959
|
+
await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
|
|
960
|
+
return await sendMessage(session, panel({
|
|
961
|
+
icon: "⚠️",
|
|
962
|
+
title: "悔棋被拒",
|
|
963
|
+
at: username,
|
|
964
|
+
body: ["落子无悔,棋局继续。"],
|
|
965
|
+
image: await renderBoard(channelId)
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
968
|
+
await undoMove(channelId);
|
|
969
|
+
await ctx.database.set("cchess_game_records", { channelId }, { isRegretRequest: false });
|
|
970
|
+
return await sendMessage(session, panel({
|
|
971
|
+
icon: "✅",
|
|
972
|
+
title: "悔棋成功",
|
|
973
|
+
at: username,
|
|
974
|
+
body: ["已同意悔棋,棋子已归原位。"],
|
|
975
|
+
tips: ["对局继续。"],
|
|
976
|
+
image: await renderBoard(channelId)
|
|
977
|
+
}));
|
|
993
978
|
}
|
|
994
|
-
__name(
|
|
979
|
+
__name(resolveRegret, "resolveRegret");
|
|
995
980
|
async function replaceAtTags(session, content) {
|
|
996
981
|
const atRegex = /<at id="(\d+)"(?: name="([^"]*)")?\/>/g;
|
|
997
982
|
let match;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cchess",
|
|
3
|
-
"description": "Koishi
|
|
4
|
-
"version": "1.0
|
|
3
|
+
"description": "Koishi 的中国象棋插件。",
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"对战"
|
|
47
47
|
],
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"koishi": "^4.18.
|
|
49
|
+
"koishi": "^4.18.11"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@koishijs/canvas": "^0.2.0",
|
package/readme.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
koishi-plugin-cchess
|
|
2
|
-
====================
|
|
3
|
-
|
|
4
|
-
[<img alt="github" src="https://img.shields.io/badge/github-araea/cchess-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/araea/koishi-plugin-cchess)
|
|
5
|
-
[<img alt="npm" src="https://img.shields.io/npm/v/koishi-plugin-cchess.svg?style=for-the-badge&color=fc8d62&logo=npm" height="20">](https://www.npmjs.com/package/koishi-plugin-cchess)
|
|
6
|
-
|
|
7
|
-
Koishi 的中国象棋插件。支持人人/人机对战。
|
|
8
|
-
|
|
9
|
-
## 使用
|
|
10
|
-
|
|
11
|
-
1. 安装 `puppeteer` 服务。
|
|
12
|
-
2. 为常用指令设置别名(如「加入」「认输」「悔棋」)。
|
|
13
|
-
|
|
14
|
-
## 指令
|
|
15
|
-
|
|
16
|
-
| 指令 | 说明 |
|
|
17
|
-
| --- | --- |
|
|
18
|
-
| `cchess.加入 [红/黑]` | 入座并选择阵营,省略则随机 |
|
|
19
|
-
| `cchess.退出` | 开局前离席 |
|
|
20
|
-
| `cchess.开始.人人对战` | 与好友手谈(至少 2 人) |
|
|
21
|
-
| `cchess.开始.人机对战` | 挑战皮卡鱼(至少 1 人) |
|
|
22
|
-
| `cchess.移动 <着法>` | 落子,亦可直接发送着法 |
|
|
23
|
-
| `cchess.悔棋.请求` | 请求悔棋,人机模式下立即生效 |
|
|
24
|
-
| `cchess.认输` | 推枰认负 |
|
|
25
|
-
| `cchess.结束` | 强制清盘 |
|
|
26
|
-
| `cchess.编辑棋盘.导入 <FEN>` | 由指定局面开局 |
|
|
27
|
-
| `cchess.编辑棋盘.导出` | 导出当前局面 |
|
|
28
|
-
| `cchess.排行榜.总胜场 [人数]` | 查看胜场榜 |
|
|
29
|
-
| `cchess.查询玩家记录 [@某人]` | 查看棋士战绩 |
|
|
30
|
-
|
|
31
|
-
## Q&A
|
|
32
|
-
|
|
33
|
-
1. 如何移动棋子?
|
|
34
|
-
|
|
35
|
-
- 纵线(炮二平五/炮8平5)或字母坐标(b2e2)。
|
|
36
|
-
- 详见 [中国象棋着法表示](https://www.xqbase.com/protocol/cchess_move.htm)。
|
|
37
|
-
|
|
38
|
-
2. 人机太强 / 太弱?
|
|
39
|
-
|
|
40
|
-
- 调整「引擎思考深度」,深度越高棋力越强,耗时也越长。
|
|
41
|
-
|
|
42
|
-
## 致谢
|
|
43
|
-
|
|
44
|
-
- [Koishi](https://koishi.chat/) - 机器人框架
|
|
45
|
-
- [风满楼]() - 棋盘与棋子素材
|
|
46
|
-
- [皮卡鱼](https://xiangqiai.com/#/) - 中国象棋 AI 引擎
|
|
47
|
-
- [中国象棋云库](https://www.chessdb.cn/query/) - 中国象棋棋谱
|
|
48
|
-
- [中国象棋应用规范](https://www.xqbase.com/protocol/cchess_intro.htm) - 中国象棋标准规范
|
|
49
|
-
|
|
50
|
-
## QQ 群
|
|
51
|
-
|
|
52
|
-
- 956758505
|
|
53
|
-
|
|
54
|
-
<br>
|
|
55
|
-
|
|
56
|
-
#### License
|
|
57
|
-
|
|
58
|
-
<sup>
|
|
59
|
-
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
|
|
60
|
-
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
|
|
61
|
-
</sup>
|
|
62
|
-
|
|
63
|
-
<br>
|
|
64
|
-
|
|
65
|
-
<sub>
|
|
66
|
-
Unless you explicitly state otherwise, any contribution intentionally submitted
|
|
67
|
-
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
|
|
68
|
-
be dual licensed as above, without any additional terms or conditions.
|
|
69
|
-
</sub>
|