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 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\n1. \u5B89\u88C5 `puppeteer` \u670D\u52A1\u3002\n2. \u4E3A\u5E38\u7528\u6307\u4EE4\u8BBE\u7F6E\u522B\u540D\uFF08\u5982\u300C\u52A0\u5165\u300D\u300C\u8BA4\u8F93\u300D\u300C\u6094\u68CB\u300D\uFF09\u3002\n\n## \u6307\u4EE4\n\n| \u6307\u4EE4 | \u8BF4\u660E |\n| --- | --- |\n| `cchess.\u52A0\u5165 [\u7EA2/\u9ED1]` | \u5165\u5EA7\u5E76\u9009\u62E9\u9635\u8425\uFF0C\u7701\u7565\u5219\u968F\u673A |\n| `cchess.\u9000\u51FA` | \u5F00\u5C40\u524D\u79BB\u5E2D |\n| `cchess.\u5F00\u59CB.\u4EBA\u4EBA\u5BF9\u6218` | \u4E0E\u597D\u53CB\u624B\u8C08\uFF08\u81F3\u5C11 2 \u4EBA\uFF09 |\n| `cchess.\u5F00\u59CB.\u4EBA\u673A\u5BF9\u6218` | \u6311\u6218\u76AE\u5361\u9C7C\uFF08\u81F3\u5C11 1 \u4EBA\uFF09 |\n| `cchess.\u79FB\u52A8 <\u7740\u6CD5>` | \u843D\u5B50\uFF0C\u4EA6\u53EF\u76F4\u63A5\u53D1\u9001\u7740\u6CD5 |\n| `cchess.\u6094\u68CB.\u8BF7\u6C42` | \u8BF7\u6C42\u6094\u68CB\uFF0C\u4EBA\u673A\u6A21\u5F0F\u4E0B\u7ACB\u5373\u751F\u6548 |\n| `cchess.\u8BA4\u8F93` | \u63A8\u67B0\u8BA4\u8D1F |\n| `cchess.\u7ED3\u675F` | \u5F3A\u5236\u6E05\u76D8 |\n| `cchess.\u7F16\u8F91\u68CB\u76D8.\u5BFC\u5165 <FEN>` | \u7531\u6307\u5B9A\u5C40\u9762\u5F00\u5C40 |\n| `cchess.\u7F16\u8F91\u68CB\u76D8.\u5BFC\u51FA` | \u5BFC\u51FA\u5F53\u524D\u5C40\u9762 |\n| `cchess.\u6392\u884C\u699C.\u603B\u80DC\u573A [\u4EBA\u6570]` | \u67E5\u770B\u80DC\u573A\u699C |\n| `cchess.\u67E5\u8BE2\u73A9\u5BB6\u8BB0\u5F55 [@\u67D0\u4EBA]` | \u67E5\u770B\u68CB\u58EB\u6218\u7EE9 |\n\n## Q&A\n\n1. \u5982\u4F55\u79FB\u52A8\u68CB\u5B50\uFF1F\n\n- \u7EB5\u7EBF\uFF08\u70AE\u4E8C\u5E73\u4E94/\u70AE8\u5E735\uFF09\u6216\u5B57\u6BCD\u5750\u6807\uFF08b2e2\uFF09\u3002\n- \u8BE6\u89C1 [\u4E2D\u56FD\u8C61\u68CB\u7740\u6CD5\u8868\u793A](https://www.xqbase.com/protocol/cchess_move.htm)\u3002\n\n2. \u4EBA\u673A\u592A\u5F3A/\u592A\u5F31\uFF1F\n\n- \u8C03\u6574\u300C\u5F15\u64CE\u601D\u8003\u6DF1\u5EA6\u300D\uFF0C\u6DF1\u5EA6\u8D8A\u9AD8\u68CB\u529B\u8D8A\u5F3A\uFF0C\u8017\u65F6\u4E5F\u8D8A\u957F\u3002\n\n## QQ \u7FA4\n\n- 956758505";
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
- 1. 安装 \`puppeteer\` 服务。
47
- 2. 为常用指令设置别名(如「加入」「认输」「悔棋」)。
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.开始.人人对战\` | 与好友手谈(至少 2 人) |
56
- | \`cchess.开始.人机对战\` | 挑战皮卡鱼(至少 1 人) |
57
- | \`cchess.移动 <着法>\` | 落子,亦可直接发送着法 |
58
- | \`cchess.悔棋.请求\` | 请求悔棋,人机模式下立即生效 |
59
- | \`cchess.认输\` | 推枰认负 |
60
- | \`cchess.结束\` | 强制清盘 |
61
- | \`cchess.编辑棋盘.导入 <FEN>\` | 由指定局面开局 |
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(`👤 @${at}`);
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.map((tip) => `💡 ${tip}`));
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: ["实在下不动了,可发送「cchess.认输」体面收官"]
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: ["期待下次与您对坐手谈。", field("剩余人数", `${playersNum - 1} 人`)]
319
+ body: ["已退出本局。", field("剩余人数", `${playersNum - 1} 人`)]
313
320
  }));
314
321
  }
315
322
  });
316
- ctx.command("cchess.开始", "开始游戏指令帮助").action(async ({ session }) => {
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: ["发送「cchess.悔棋.同意」或「cchess.悔棋.拒绝」"]
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.悔棋", "悔棋指令帮助").action(async ({ session }) => {
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: ["对方可发送「cchess.悔棋.同意」或「cchess.悔棋.拒绝」"]
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("您的阵营", withSideIcon(playerRecord[0].side)), field("当前轮走", withSideIcon(sideString))],
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.查看云库残局", "云库残局指令帮助").action(async ({ session }) => {
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: "云库残局 · DTC 统计",
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.编辑棋盘", "编辑棋盘指令帮助").action(async ({ session }) => {
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.开始.人人对战」或「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.编辑棋盘.使用方法", "查看编辑棋盘的fen使用方法").action(async ({ session }) => {
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 (typeof number !== "number" || isNaN(number) || number < 0) {
807
- return await sendMessage(session, invalidLeaderboardSizePanel(username));
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
- return await getLeaderboard(session, "lose", "总输场排行榜", "🍂", number);
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.开始.人人对战」与好友手谈", "「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.认输」体面收官", "「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("您的阵营", withSideIcon(yourSide)), field("当前轮走", withSideIcon(currentSide))],
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: ["请输入大于等于 0 的整数。"],
936
- tips: ["例如:cchess.排行榜.总胜场 10"]
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 = "🏆", at, extra = [], withBoard = false } = options;
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 checkRegretDecision(session, action) {
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: [`当前没有待答复的请求,无从${action}。`]
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
- return null;
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(checkRegretDecision, "checkRegretDecision");
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.7",
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.9"
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>